Compare commits
25 Commits
c2fb7bac03
...
75df38ff5d
Author | SHA1 | Date | |
---|---|---|---|
75df38ff5d | |||
b8bb36bfd3 | |||
8fcbb7aced | |||
1ba5b9ec41 | |||
443ff86aee | |||
5ee7071dff | |||
1802e54caf | |||
3dceb17085 | |||
ca5b29c914 | |||
264f13e523 | |||
e9360fb3d5 | |||
44c8cf77bb | |||
632c2b3480 | |||
852e60dd14 | |||
85c4559d1f | |||
7195c7fba6 | |||
ad457b428a | |||
cc11491106 | |||
a0a0697f2d | |||
2ce4fe3cc8 | |||
f9be5229e7 | |||
b95d3039bb | |||
3c250e9064 | |||
6f80ef35a1 | |||
1a508a42ef |
@ -1,6 +1,6 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: memex
|
||||
name: memEx
|
||||
|
||||
steps:
|
||||
- name: restore-cache
|
||||
|
@ -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;
|
||||
@apply text-primary-400 placeholder-primary-600;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@ -44,11 +44,7 @@
|
||||
}
|
||||
|
||||
.hr {
|
||||
@apply border border-primary-400 w-full max-w-2xl;
|
||||
}
|
||||
|
||||
.hr-light {
|
||||
@apply border border-primary-600 w-full max-w-2xl;
|
||||
@apply mx-auto border border-primary-600 w-full max-w-2xl;
|
||||
}
|
||||
|
||||
.link {
|
||||
|
@ -29,7 +29,9 @@ import topbar from '../vendor/topbar'
|
||||
import MaintainAttrs from './maintain_attrs'
|
||||
import Alpine from 'alpinejs'
|
||||
|
||||
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute('content')
|
||||
const csrfTokenElement = document.querySelector("meta[name='csrf-token']")
|
||||
let csrfToken
|
||||
if (csrfTokenElement) { csrfToken = csrfTokenElement.getAttribute('content') }
|
||||
const liveSocket = new LiveSocket('/live', Socket, {
|
||||
dom: {
|
||||
onBeforeElUpdated (from, to) {
|
||||
@ -45,7 +47,7 @@ window.Alpine = Alpine
|
||||
Alpine.start()
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' })
|
||||
topbar.config({ barThickness: 1, barColors: { 0: '#fff' }, shadowColor: 'rgba(0, 0, 0, .3)' })
|
||||
window.addEventListener('phx:page-loading-start', info => topbar.show())
|
||||
window.addEventListener('phx:page-loading-stop', info => topbar.hide())
|
||||
|
||||
|
@ -23,7 +23,7 @@ defmodule Memex.Accounts do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec get_user_by_email(String.t()) :: User.t() | nil
|
||||
@spec get_user_by_email(email :: String.t()) :: User.t() | nil
|
||||
def get_user_by_email(email) when is_binary(email), do: Repo.get_by(User, email: email)
|
||||
|
||||
@doc """
|
||||
@ -38,7 +38,7 @@ defmodule Memex.Accounts do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec get_user_by_email_and_password(String.t(), String.t()) ::
|
||||
@spec get_user_by_email_and_password(email :: String.t(), password :: String.t()) ::
|
||||
User.t() | nil
|
||||
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(:admin | :user) :: [User.t()]
|
||||
@spec list_users_by_role(User.role()) :: [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,15 +106,21 @@ defmodule Memex.Accounts do
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec register_user(map()) :: {:ok, User.t()} | {:error, Changeset.t(User.new_user())}
|
||||
@spec register_user(attrs :: map()) :: {:ok, User.t()} | {:error, User.changeset()}
|
||||
def register_user(attrs) do
|
||||
# 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"
|
||||
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"
|
||||
|
||||
%User{} |> User.registration_changeset(attrs |> Map.put("role", role)) |> Repo.insert()
|
||||
User.registration_changeset(attrs) |> User.role_changeset(role)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{add_user: user}} -> {:ok, user}
|
||||
{:error, :add_user, changeset, _changes_so_far} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -126,12 +132,10 @@ defmodule Memex.Accounts do
|
||||
%Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
@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)
|
||||
@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)
|
||||
|
||||
## Settings
|
||||
|
||||
@ -144,7 +148,7 @@ defmodule Memex.Accounts do
|
||||
%Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
@spec change_user_email(User.t(), map()) :: Changeset.t(User.t())
|
||||
@spec change_user_email(User.t(), attrs :: map()) :: User.changeset()
|
||||
def change_user_email(user, attrs \\ %{}), do: User.email_changeset(user, attrs)
|
||||
|
||||
@doc """
|
||||
@ -156,7 +160,7 @@ defmodule Memex.Accounts do
|
||||
%Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
@spec change_user_role(User.t(), atom()) :: Changeset.t(User.t())
|
||||
@spec change_user_role(User.t(), User.role()) :: User.changeset()
|
||||
def change_user_role(user, role), do: User.role_changeset(user, role)
|
||||
|
||||
@doc """
|
||||
@ -172,8 +176,8 @@ defmodule Memex.Accounts do
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec apply_user_email(User.t(), String.t(), map()) ::
|
||||
{:ok, User.t()} | {:error, Changeset.t(User.t())}
|
||||
@spec apply_user_email(User.t(), password :: String.t(), attrs :: map()) ::
|
||||
{:ok, User.t()} | {:error, User.changeset()}
|
||||
def apply_user_email(user, password, attrs) do
|
||||
user
|
||||
|> User.email_changeset(attrs)
|
||||
@ -187,7 +191,7 @@ defmodule Memex.Accounts do
|
||||
If the token matches, the user email is updated and the token is deleted.
|
||||
The confirmed_at date is also updated to the current time.
|
||||
"""
|
||||
@spec update_user_email(User.t(), String.t()) :: :ok | :error
|
||||
@spec update_user_email(User.t(), token :: String.t()) :: :ok | :error
|
||||
def update_user_email(user, token) do
|
||||
context = "change:#{user.email}"
|
||||
|
||||
@ -200,7 +204,7 @@ defmodule Memex.Accounts do
|
||||
end
|
||||
end
|
||||
|
||||
@spec user_email_multi(User.t(), String.t(), String.t()) :: Multi.t()
|
||||
@spec user_email_multi(User.t(), email :: String.t(), context :: String.t()) :: Multi.t()
|
||||
defp user_email_multi(user, email, context) do
|
||||
changeset = user |> User.email_changeset(%{email: email}) |> User.confirm_changeset()
|
||||
|
||||
@ -218,7 +222,8 @@ defmodule Memex.Accounts do
|
||||
{:ok, %{to: ..., body: ...}}
|
||||
|
||||
"""
|
||||
@spec deliver_update_email_instructions(User.t(), String.t(), function) :: Job.t()
|
||||
@spec deliver_update_email_instructions(User.t(), current_email :: String.t(), function) ::
|
||||
Job.t()
|
||||
def deliver_update_email_instructions(user, current_email, update_email_url_fun)
|
||||
when is_function(update_email_url_fun, 1) do
|
||||
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
|
||||
@ -235,7 +240,7 @@ defmodule Memex.Accounts do
|
||||
%Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
@spec change_user_password(User.t(), map()) :: Changeset.t(User.t())
|
||||
@spec change_user_password(User.t(), attrs :: map()) :: User.changeset()
|
||||
def change_user_password(user, attrs \\ %{}),
|
||||
do: User.password_changeset(user, attrs, hash_password: false)
|
||||
|
||||
@ -251,8 +256,8 @@ defmodule Memex.Accounts do
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec update_user_password(User.t(), String.t(), map()) ::
|
||||
{:ok, User.t()} | {:error, Changeset.t(User.t())}
|
||||
@spec update_user_password(User.t(), password :: String.t(), attrs :: map()) ::
|
||||
{:ok, User.t()} | {:error, User.changeset()}
|
||||
def update_user_password(user, password, attrs) do
|
||||
changeset =
|
||||
user
|
||||
@ -278,7 +283,7 @@ defmodule Memex.Accounts do
|
||||
%Changeset{data: %User{}}
|
||||
|
||||
"""
|
||||
@spec change_user_locale(User.t()) :: Changeset.t(User.t())
|
||||
@spec change_user_locale(User.t()) :: User.changeset()
|
||||
def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale)
|
||||
|
||||
@doc """
|
||||
@ -294,7 +299,7 @@ defmodule Memex.Accounts do
|
||||
|
||||
"""
|
||||
@spec update_user_locale(User.t(), locale :: String.t()) ::
|
||||
{:ok, User.t()} | {:error, Changeset.t(User.t())}
|
||||
{:ok, User.t()} | {:error, User.changeset()}
|
||||
def update_user_locale(user, locale),
|
||||
do: user |> User.locale_changeset(locale) |> Repo.update()
|
||||
|
||||
@ -310,7 +315,7 @@ defmodule Memex.Accounts do
|
||||
%User{}
|
||||
|
||||
"""
|
||||
@spec delete_user!(User.t(), User.t()) :: User.t()
|
||||
@spec delete_user!(user_to_delete :: User.t(), User.t()) :: User.t()
|
||||
def delete_user!(user, %User{role: :admin}), do: user |> Repo.delete!()
|
||||
def delete_user!(%User{id: user_id} = user, %User{id: user_id}), do: user |> Repo.delete!()
|
||||
|
||||
@ -329,7 +334,7 @@ defmodule Memex.Accounts do
|
||||
@doc """
|
||||
Gets the user with the given signed token.
|
||||
"""
|
||||
@spec get_user_by_session_token(String.t()) :: User.t()
|
||||
@spec get_user_by_session_token(token :: String.t()) :: User.t()
|
||||
def get_user_by_session_token(token) do
|
||||
{:ok, query} = UserToken.verify_session_token_query(token)
|
||||
Repo.one(query)
|
||||
@ -338,7 +343,7 @@ defmodule Memex.Accounts do
|
||||
@doc """
|
||||
Deletes the signed token with the given context.
|
||||
"""
|
||||
@spec delete_session_token(String.t()) :: :ok
|
||||
@spec delete_session_token(token :: String.t()) :: :ok
|
||||
def delete_session_token(token) do
|
||||
Repo.delete_all(UserToken.token_and_context_query(token, "session"))
|
||||
:ok
|
||||
@ -358,10 +363,16 @@ defmodule Memex.Accounts do
|
||||
"""
|
||||
@spec is_admin?(User.t()) :: boolean()
|
||||
def is_admin?(%User{id: user_id}) do
|
||||
Repo.one(from u in User, where: u.id == ^user_id and u.role == :admin)
|
||||
|> is_nil()
|
||||
Repo.exists?(from u in User, where: u.id == ^user_id and u.role == :admin)
|
||||
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 """
|
||||
@ -394,7 +405,7 @@ defmodule Memex.Accounts do
|
||||
If the token matches, the user account is marked as confirmed
|
||||
and the token is deleted.
|
||||
"""
|
||||
@spec confirm_user(String.t()) :: {:ok, User.t()} | atom()
|
||||
@spec confirm_user(token :: String.t()) :: {:ok, User.t()} | atom()
|
||||
def confirm_user(token) do
|
||||
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
|
||||
%User{} = user <- Repo.one(query),
|
||||
@ -443,7 +454,7 @@ defmodule Memex.Accounts do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec get_user_by_reset_password_token(String.t()) :: User.t() | nil
|
||||
@spec get_user_by_reset_password_token(token :: String.t()) :: User.t() | nil
|
||||
def get_user_by_reset_password_token(token) do
|
||||
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
|
||||
%User{} = user <- Repo.one(query) do
|
||||
@ -465,7 +476,8 @@ defmodule Memex.Accounts do
|
||||
{:error, %Changeset{}}
|
||||
|
||||
"""
|
||||
@spec reset_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t(User.t())}
|
||||
@spec reset_user_password(User.t(), attrs :: map()) ::
|
||||
{:ok, User.t()} | {:error, User.changeset()}
|
||||
def reset_user_password(user, attrs) do
|
||||
Multi.new()
|
||||
|> Multi.update(:user, User.password_changeset(user, attrs))
|
||||
|
@ -7,7 +7,7 @@ defmodule Memex.Accounts.User do
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Invites.Invite}
|
||||
alias Memex.Invites.Invite
|
||||
|
||||
@derive {Inspect, except: [:password]}
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@ -25,20 +25,22 @@ defmodule Memex.Accounts.User do
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %User{
|
||||
@type t :: %__MODULE__{
|
||||
id: id(),
|
||||
email: String.t(),
|
||||
password: String.t(),
|
||||
hashed_password: String.t(),
|
||||
confirmed_at: NaiveDateTime.t(),
|
||||
role: atom(),
|
||||
role: role(),
|
||||
invites: [Invite.t()],
|
||||
locale: String.t() | nil,
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type new_user :: %User{}
|
||||
@type new_user :: %__MODULE__{}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t() | new_user())
|
||||
@type role :: :user | :admin | String.t()
|
||||
|
||||
@doc """
|
||||
A user changeset for registration.
|
||||
@ -57,12 +59,11 @@ defmodule Memex.Accounts.User do
|
||||
validations on a LiveView form), this option can be set to `false`.
|
||||
Defaults to `true`.
|
||||
"""
|
||||
@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])
|
||||
@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])
|
||||
|> validate_email()
|
||||
|> validate_password(opts)
|
||||
end
|
||||
@ -71,12 +72,12 @@ defmodule Memex.Accounts.User do
|
||||
A user changeset for role.
|
||||
|
||||
"""
|
||||
@spec role_changeset(t(), role :: atom()) :: Changeset.t(t())
|
||||
@spec role_changeset(t() | new_user() | changeset(), role()) :: changeset()
|
||||
def role_changeset(user, role) do
|
||||
user |> cast(%{"role" => role}, [:role])
|
||||
end
|
||||
|
||||
@spec validate_email(Changeset.t(t() | new_user())) :: Changeset.t(t() | new_user())
|
||||
@spec validate_email(changeset()) :: changeset()
|
||||
defp validate_email(changeset) do
|
||||
changeset
|
||||
|> validate_required([:email])
|
||||
@ -88,8 +89,7 @@ defmodule Memex.Accounts.User do
|
||||
|> unique_constraint(:email)
|
||||
end
|
||||
|
||||
@spec validate_password(Changeset.t(t() | new_user()), opts :: keyword()) ::
|
||||
Changeset.t(t() | new_user())
|
||||
@spec validate_password(changeset(), opts :: keyword()) :: changeset()
|
||||
defp validate_password(changeset, opts) do
|
||||
changeset
|
||||
|> validate_required([:password])
|
||||
@ -100,8 +100,7 @@ defmodule Memex.Accounts.User do
|
||||
|> maybe_hash_password(opts)
|
||||
end
|
||||
|
||||
@spec maybe_hash_password(Changeset.t(t() | new_user()), opts :: keyword()) ::
|
||||
Changeset.t(t() | new_user())
|
||||
@spec maybe_hash_password(changeset(), opts :: keyword()) :: changeset()
|
||||
defp maybe_hash_password(changeset, opts) do
|
||||
hash_password? = Keyword.get(opts, :hash_password, true)
|
||||
password = get_change(changeset, :password)
|
||||
@ -120,7 +119,7 @@ defmodule Memex.Accounts.User do
|
||||
|
||||
It requires the email to change otherwise an error is added.
|
||||
"""
|
||||
@spec email_changeset(t(), attrs :: map()) :: Changeset.t(t())
|
||||
@spec email_changeset(t(), attrs :: map()) :: changeset()
|
||||
def email_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:email])
|
||||
@ -143,8 +142,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.t(t())
|
||||
@spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: Changeset.t(t())
|
||||
@spec password_changeset(t(), attrs :: map()) :: changeset()
|
||||
@spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: changeset()
|
||||
def password_changeset(user, attrs, opts \\ []) do
|
||||
user
|
||||
|> cast(attrs, [:password])
|
||||
@ -155,7 +154,7 @@ defmodule Memex.Accounts.User do
|
||||
@doc """
|
||||
Confirms the account by setting `confirmed_at`.
|
||||
"""
|
||||
@spec confirm_changeset(t() | Changeset.t(t())) :: Changeset.t(t())
|
||||
@spec confirm_changeset(t() | changeset()) :: changeset()
|
||||
def confirm_changeset(user_or_changeset) do
|
||||
now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
|
||||
user_or_changeset |> change(confirmed_at: now)
|
||||
@ -168,7 +167,7 @@ defmodule Memex.Accounts.User do
|
||||
`Bcrypt.no_user_verify/0` to avoid timing attacks.
|
||||
"""
|
||||
@spec valid_password?(t(), String.t()) :: boolean()
|
||||
def valid_password?(%User{hashed_password: hashed_password}, password)
|
||||
def valid_password?(%__MODULE__{hashed_password: hashed_password}, password)
|
||||
when is_binary(hashed_password) and byte_size(password) > 0 do
|
||||
Bcrypt.verify_pass(password, hashed_password)
|
||||
end
|
||||
@ -181,7 +180,7 @@ defmodule Memex.Accounts.User do
|
||||
@doc """
|
||||
Validates the current password otherwise adds an error to the changeset.
|
||||
"""
|
||||
@spec validate_current_password(Changeset.t(t()), String.t()) :: Changeset.t(t())
|
||||
@spec validate_current_password(changeset(), String.t()) :: changeset()
|
||||
def validate_current_password(changeset, password) do
|
||||
if valid_password?(changeset.data, password),
|
||||
do: changeset,
|
||||
@ -191,7 +190,7 @@ defmodule Memex.Accounts.User do
|
||||
@doc """
|
||||
A changeset for changing the user's locale
|
||||
"""
|
||||
@spec locale_changeset(t() | Changeset.t(t()), locale :: String.t() | nil) :: Changeset.t(t())
|
||||
@spec locale_changeset(t() | changeset(), locale :: String.t() | nil) :: changeset()
|
||||
def locale_changeset(user_or_changeset, locale) do
|
||||
user_or_changeset
|
||||
|> cast(%{"locale" => locale}, [:locale])
|
||||
|
@ -4,21 +4,89 @@ defmodule Memex.Contexts do
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.Repo
|
||||
|
||||
alias Memex.Contexts.Context
|
||||
alias Ecto.Changeset
|
||||
alias Memex.{Accounts.User, Contexts.Context, Repo}
|
||||
|
||||
@doc """
|
||||
Returns the list of contexts.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_contexts()
|
||||
iex> list_contexts(%User{id: 123})
|
||||
[%Context{}, ...]
|
||||
|
||||
iex> list_contexts("my context", %User{id: 123})
|
||||
[%Context{slug: "my context"}, ...]
|
||||
|
||||
"""
|
||||
def list_contexts do
|
||||
Repo.all(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
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -28,31 +96,78 @@ defmodule Memex.Contexts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_context!(123)
|
||||
iex> get_context!(123, %User{id: 123})
|
||||
%Context{}
|
||||
|
||||
iex> get_context!(456)
|
||||
iex> get_context!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_context!(id), do: Repo.get!(Context, id)
|
||||
@spec get_context!(Context.id(), User.t()) :: Context.t()
|
||||
def get_context!(id, %{id: user_id}) do
|
||||
Repo.one!(
|
||||
from c in Context,
|
||||
where: c.id == ^id,
|
||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
def get_context!(id, _invalid_user) do
|
||||
Repo.one!(
|
||||
from c in Context,
|
||||
where: c.id == ^id,
|
||||
where: c.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single context by a slug.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Context does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_context_by_slug("my-context", %User{id: 123})
|
||||
%Context{}
|
||||
|
||||
iex> get_context_by_slug("my-context", %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_context_by_slug(Context.slug(), User.t()) :: Context.t() | nil
|
||||
def get_context_by_slug(slug, %{id: user_id}) do
|
||||
Repo.one(
|
||||
from c in Context,
|
||||
where: c.slug == ^slug,
|
||||
where: c.user_id == ^user_id or c.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
def get_context_by_slug(slug, _invalid_user) do
|
||||
Repo.one(
|
||||
from c in Context,
|
||||
where: c.slug == ^slug,
|
||||
where: c.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a context.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_context(%{field: value})
|
||||
iex> create_context(%{field: value}, %User{id: 123})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> create_context(%{field: bad_value})
|
||||
iex> create_context(%{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_context(attrs \\ %{}) do
|
||||
%Context{}
|
||||
|> Context.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
@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()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -60,16 +175,18 @@ defmodule Memex.Contexts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_context(context, %{field: new_value})
|
||||
iex> update_context(context, %{field: new_value}, %User{id: 123})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> update_context(context, %{field: bad_value})
|
||||
iex> update_context(context, %{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_context(%Context{} = context, attrs) do
|
||||
@spec update_context(Context.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Context.t()} | {:error, Context.changeset()}
|
||||
def update_context(%Context{} = context, attrs, user) do
|
||||
context
|
||||
|> Context.changeset(attrs)
|
||||
|> Context.update_changeset(attrs, user)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@ -78,15 +195,24 @@ defmodule Memex.Contexts do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_context(context)
|
||||
iex> delete_context(%Context{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> delete_context(context)
|
||||
iex> delete_context(%Context{user_id: 123}, %User{role: :admin})
|
||||
{:ok, %Context{}}
|
||||
|
||||
iex> delete_context(%Context{}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_context(%Context{} = context) do
|
||||
Repo.delete(context)
|
||||
@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()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -98,7 +224,23 @@ defmodule Memex.Contexts do
|
||||
%Ecto.Changeset{data: %Context{}}
|
||||
|
||||
"""
|
||||
def change_context(%Context{} = context, attrs \\ %{}) do
|
||||
Context.changeset(context, attrs)
|
||||
@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()
|
||||
end
|
||||
end
|
||||
|
@ -1,22 +1,77 @@
|
||||
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 :tag, {:array, :string}
|
||||
field :title, :string
|
||||
field :tags, {:array, :string}
|
||||
field :tags_string, :string, virtual: true
|
||||
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
|
||||
def changeset(context, attrs) do
|
||||
context
|
||||
|> cast(attrs, [:title, :content, :tag, :visibility])
|
||||
|> validate_required([:title, :content, :tag, :visibility])
|
||||
@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])
|
||||
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
|
||||
|
@ -1,20 +0,0 @@
|
||||
defmodule Memex.Contexts.ContextNote do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "context_notes" do
|
||||
field :context_id, :binary_id
|
||||
field :note_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(context_note, attrs) do
|
||||
context_note
|
||||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
end
|
||||
end
|
@ -15,13 +15,16 @@ 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() | nil) :: [Note.t()]
|
||||
@spec list_notes(search :: String.t() | nil, User.t() | nil) :: [Note.t()]
|
||||
@spec list_notes(User.t()) :: [Note.t()]
|
||||
@spec list_notes(search :: String.t() | nil, User.t()) :: [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.title)
|
||||
Repo.all(from n in Note, where: n.user_id == ^user_id, order_by: n.slug)
|
||||
end
|
||||
|
||||
def list_notes(search, %{id: user_id}) when search |> is_binary() do
|
||||
@ -52,13 +55,16 @@ 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.title)
|
||||
Repo.all(from n in Note, where: n.visibility == :public, order_by: n.slug)
|
||||
end
|
||||
|
||||
def list_public_notes(search) when search |> is_binary() do
|
||||
@ -113,6 +119,37 @@ 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.
|
||||
|
||||
@ -125,8 +162,8 @@ defmodule Memex.Notes do
|
||||
{:error, %Ecto.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()}
|
||||
@spec create_note(User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
||||
@spec create_note(attrs :: map(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
||||
def create_note(attrs \\ %{}, user) do
|
||||
Note.create_changeset(attrs, user) |> Repo.insert()
|
||||
end
|
||||
@ -144,7 +181,7 @@ defmodule Memex.Notes do
|
||||
|
||||
"""
|
||||
@spec update_note(Note.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Note.t()} | {:error, Changeset.t()}
|
||||
{:ok, Note.t()} | {:error, Note.changeset()}
|
||||
def update_note(%Note{} = note, attrs, user) do
|
||||
note
|
||||
|> Note.update_changeset(attrs, user)
|
||||
@ -156,18 +193,25 @@ defmodule Memex.Notes do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_note(note, %User{id: 123})
|
||||
iex> delete_note(%Note{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Note{}}
|
||||
|
||||
iex> delete_note(note, %User{id: 123})
|
||||
iex> delete_note(%Note{}, %User{role: :admin})
|
||||
{:ok, %Note{}}
|
||||
|
||||
iex> delete_note(%Note{}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Changeset.t()}
|
||||
@spec delete_note(Note.t(), User.t()) :: {:ok, Note.t()} | {:error, Note.changeset()}
|
||||
def delete_note(%Note{user_id: user_id} = note, %{id: user_id}) do
|
||||
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.
|
||||
|
||||
@ -176,12 +220,12 @@ defmodule Memex.Notes do
|
||||
iex> change_note(note, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Note{}}
|
||||
|
||||
iex> change_note(note, %{title: "new title"}, %User{id: 123})
|
||||
iex> change_note(note, %{slug: "new slug"}, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Note{}}
|
||||
|
||||
"""
|
||||
@spec change_note(Note.t(), User.t()) :: Changeset.t(Note.t())
|
||||
@spec change_note(Note.t(), attrs :: map(), User.t()) :: Changeset.t(Note.t())
|
||||
@spec change_note(Note.t(), User.t()) :: Note.changeset()
|
||||
@spec change_note(Note.t(), attrs :: map(), User.t()) :: Note.changeset()
|
||||
def change_note(%Note{} = note, attrs \\ %{}, user) do
|
||||
note |> Note.update_changeset(attrs, user)
|
||||
end
|
||||
@ -189,7 +233,7 @@ defmodule Memex.Notes do
|
||||
@doc """
|
||||
Gets a canonical string representation of the `:tags` field for a Note
|
||||
"""
|
||||
@spec get_tags_string(Note.t() | Changeset.t() | [String.t()] | nil) :: String.t()
|
||||
@spec get_tags_string(Note.t() | Note.changeset() | [String.t()] | nil) :: String.t()
|
||||
def get_tags_string(nil), do: ""
|
||||
def get_tags_string(tags) when tags |> is_list(), do: tags |> Enum.join(",")
|
||||
def get_tags_string(%Note{tags: tags}), do: tags |> get_tags_string()
|
||||
|
@ -4,16 +4,17 @@ defmodule Memex.Notes.Note do
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import MemexWeb.Gettext
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Notes.Note}
|
||||
alias Memex.Accounts.User
|
||||
|
||||
@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
|
||||
@ -21,29 +22,47 @@ defmodule Memex.Notes.Note do
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %Note{}
|
||||
@type t :: %__MODULE__{
|
||||
slug: slug(),
|
||||
content: String.t(),
|
||||
tags: [String.t()] | nil,
|
||||
tags_string: String.t(),
|
||||
visibility: :public | :private | :unlisted,
|
||||
user: User.t() | Ecto.Association.NotLoaded.t(),
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type 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
|
||||
%Note{}
|
||||
|> cast(attrs, [:title, :content, :tags, :visibility])
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||
|> change(user_id: user_id)
|
||||
|> cast_tags_string(attrs)
|
||||
|> validate_required([:title, :content, :user_id, :visibility])
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :user_id, :visibility])
|
||||
end
|
||||
|
||||
@spec update_changeset(Note.t(), attrs :: map(), User.t()) :: changeset()
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) :: changeset()
|
||||
def update_changeset(%{user_id: user_id} = note, attrs, %User{id: user_id}) do
|
||||
note
|
||||
|> cast(attrs, [:title, :content, :tags, :visibility])
|
||||
|> cast(attrs, [:slug, :content, :tags, :visibility])
|
||||
|> cast_tags_string(attrs)
|
||||
|> validate_required([:title, :content, :visibility])
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :content, :visibility])
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, %{"tags_string" => tags_string}) when is_binary(tags_string) do
|
||||
defp cast_tags_string(changeset, %{"tags_string" => tags_string})
|
||||
when tags_string |> is_binary() do
|
||||
tags =
|
||||
tags_string
|
||||
|> String.split(",", trim: true)
|
||||
|
@ -4,21 +4,88 @@ defmodule Memex.Pipelines do
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.Repo
|
||||
|
||||
alias Memex.Pipelines.Pipeline
|
||||
alias Ecto.Changeset
|
||||
alias Memex.{Accounts.User, Pipelines.Pipeline, Repo}
|
||||
|
||||
@doc """
|
||||
Returns the list of pipelines.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_pipelines()
|
||||
iex> list_pipelines(%User{id: 123})
|
||||
[%Pipeline{}, ...]
|
||||
|
||||
iex> list_pipelines("my pipeline", %User{id: 123})
|
||||
[%Pipeline{slug: "my pipeline"}, ...]
|
||||
|
||||
"""
|
||||
def list_pipelines do
|
||||
Repo.all(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
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -28,31 +95,78 @@ defmodule Memex.Pipelines do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_pipeline!(123)
|
||||
iex> get_pipeline!(123, %User{id: 123})
|
||||
%Pipeline{}
|
||||
|
||||
iex> get_pipeline!(456)
|
||||
iex> get_pipeline!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_pipeline!(id), do: Repo.get!(Pipeline, id)
|
||||
@spec get_pipeline!(Pipeline.id(), User.t()) :: Pipeline.t()
|
||||
def get_pipeline!(id, %{id: user_id}) do
|
||||
Repo.one!(
|
||||
from p in Pipeline,
|
||||
where: p.id == ^id,
|
||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
def get_pipeline!(id, _invalid_user) do
|
||||
Repo.one!(
|
||||
from p in Pipeline,
|
||||
where: p.id == ^id,
|
||||
where: p.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single pipeline by it's slug.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Pipeline does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_pipeline_by_slug("my-pipeline", %User{id: 123})
|
||||
%Pipeline{}
|
||||
|
||||
iex> get_pipeline_by_slug("my-pipeline", %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_pipeline_by_slug(Pipeline.slug(), User.t()) :: Pipeline.t() | nil
|
||||
def get_pipeline_by_slug(slug, %{id: user_id}) do
|
||||
Repo.one(
|
||||
from p in Pipeline,
|
||||
where: p.slug == ^slug,
|
||||
where: p.user_id == ^user_id or p.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
def get_pipeline_by_slug(slug, _invalid_user) do
|
||||
Repo.one(
|
||||
from p in Pipeline,
|
||||
where: p.slug == ^slug,
|
||||
where: p.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a pipeline.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_pipeline(%{field: value})
|
||||
iex> create_pipeline(%{field: value}, %User{id: 123})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> create_pipeline(%{field: bad_value})
|
||||
iex> create_pipeline(%{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_pipeline(attrs \\ %{}) do
|
||||
%Pipeline{}
|
||||
|> Pipeline.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
@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()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -60,16 +174,18 @@ defmodule Memex.Pipelines do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_pipeline(pipeline, %{field: new_value})
|
||||
iex> update_pipeline(pipeline, %{field: new_value}, %User{id: 123})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> update_pipeline(pipeline, %{field: bad_value})
|
||||
iex> update_pipeline(pipeline, %{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_pipeline(%Pipeline{} = pipeline, attrs) do
|
||||
@spec update_pipeline(Pipeline.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Pipeline.t()} | {:error, Pipeline.changeset()}
|
||||
def update_pipeline(%Pipeline{} = pipeline, attrs, user) do
|
||||
pipeline
|
||||
|> Pipeline.changeset(attrs)
|
||||
|> Pipeline.update_changeset(attrs, user)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@ -78,15 +194,24 @@ defmodule Memex.Pipelines do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_pipeline(pipeline)
|
||||
iex> delete_pipeline(%Pipeline{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> delete_pipeline(pipeline)
|
||||
iex> delete_pipeline(%Pipeline{}, %User{role: :admin})
|
||||
{:ok, %Pipeline{}}
|
||||
|
||||
iex> delete_pipeline(%Pipeline{}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_pipeline(%Pipeline{} = pipeline) do
|
||||
Repo.delete(pipeline)
|
||||
@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()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -94,11 +219,30 @@ defmodule Memex.Pipelines do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_pipeline(pipeline)
|
||||
iex> change_pipeline(pipeline, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Pipeline{}}
|
||||
|
||||
iex> change_pipeline(pipeline, %{slug: "new slug"}, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Pipeline{}}
|
||||
|
||||
"""
|
||||
def change_pipeline(%Pipeline{} = pipeline, attrs \\ %{}) do
|
||||
Pipeline.changeset(pipeline, attrs)
|
||||
@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()
|
||||
end
|
||||
end
|
||||
|
@ -1,21 +1,78 @@
|
||||
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 :title, :string
|
||||
field :tags, {:array, :string}
|
||||
field :tags_string, :string, virtual: true
|
||||
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
|
||||
def changeset(pipeline, attrs) do
|
||||
pipeline
|
||||
|> cast(attrs, [:title, :description, :visibility])
|
||||
|> validate_required([:title, :description, :visibility])
|
||||
@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
|
||||
pipeline
|
||||
|> cast(attrs, [:slug, :description, :tags, :visibility])
|
||||
|> cast_tags_string(attrs)
|
||||
|> validate_format(:slug, ~r/^[\p{L}\p{N}\-]+$/,
|
||||
message: dgettext("errors", "invalid format: only numbers, letters and hyphen are accepted")
|
||||
)
|
||||
|> validate_required([:slug, :visibility])
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, %{"tags_string" => tags_string})
|
||||
when tags_string |> is_binary() do
|
||||
tags =
|
||||
tags_string
|
||||
|> String.split(",", trim: true)
|
||||
|> Enum.map(fn str -> str |> String.trim() end)
|
||||
|> Enum.sort()
|
||||
|
||||
changeset |> change(tags: tags)
|
||||
end
|
||||
|
||||
defp cast_tags_string(changeset, _attrs), do: changeset
|
||||
end
|
||||
|
71
lib/memex/pipelines/step.ex
Normal file
71
lib/memex/pipelines/step.ex
Normal file
@ -0,0 +1,71 @@
|
||||
defmodule Memex.Pipelines.Steps.Step do
|
||||
@moduledoc """
|
||||
Represents a step taken while executing a pipeline
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Memex.{Accounts.User, Pipelines.Pipeline}
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "steps" do
|
||||
field :title, :string
|
||||
field :content, :string
|
||||
field :position, :integer
|
||||
|
||||
belongs_to :pipeline, Pipeline
|
||||
belongs_to :user, User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
title: String.t(),
|
||||
content: String.t(),
|
||||
position: non_neg_integer(),
|
||||
pipeline: Pipeline.t() | Ecto.Association.NotLoaded.t(),
|
||||
pipeline_id: Pipeline.id(),
|
||||
user: User.t() | Ecto.Association.NotLoaded.t(),
|
||||
user_id: User.id(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
updated_at: NaiveDateTime.t()
|
||||
}
|
||||
@type id :: UUID.t()
|
||||
@type changeset :: Changeset.t(t())
|
||||
|
||||
@doc false
|
||||
@spec create_changeset(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
||||
changeset()
|
||||
def create_changeset(attrs, position, %Pipeline{id: pipeline_id, user_id: user_id}, %User{
|
||||
id: user_id
|
||||
}) do
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, [:title, :content])
|
||||
|> change(pipeline_id: pipeline_id, user_id: user_id, position: position)
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
end
|
||||
|
||||
@spec update_changeset(t(), attrs :: map(), User.t()) ::
|
||||
changeset()
|
||||
def update_changeset(
|
||||
%{user_id: user_id} = step,
|
||||
attrs,
|
||||
%User{id: user_id}
|
||||
) do
|
||||
step
|
||||
|> cast(attrs, [:title, :content])
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
end
|
||||
|
||||
@spec position_changeset(t(), position :: non_neg_integer(), User.t()) :: changeset()
|
||||
def position_changeset(
|
||||
%{user_id: user_id} = step,
|
||||
position,
|
||||
%User{id: user_id}
|
||||
) do
|
||||
step
|
||||
|> change(position: position)
|
||||
|> validate_required([:title, :content, :user_id, :position])
|
||||
end
|
||||
end
|
238
lib/memex/pipelines/steps.ex
Normal file
238
lib/memex/pipelines/steps.ex
Normal file
@ -0,0 +1,238 @@
|
||||
defmodule Memex.Pipelines.Steps do
|
||||
@moduledoc """
|
||||
The context for steps within a pipeline
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Memex.{Accounts.User, Repo}
|
||||
alias Memex.Pipelines.{Pipeline, Steps.Step}
|
||||
|
||||
@doc """
|
||||
Returns the list of steps.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_steps(%User{id: 123})
|
||||
[%Step{}, ...]
|
||||
|
||||
iex> list_steps("my step", %User{id: 123})
|
||||
[%Step{title: "my step"}, ...]
|
||||
|
||||
"""
|
||||
@spec list_steps(Pipeline.t(), User.t()) :: [Step.t()]
|
||||
def list_steps(%{id: pipeline_id}, %{id: user_id}) do
|
||||
Repo.all(
|
||||
from s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
where: s.user_id == ^user_id,
|
||||
order_by: s.position
|
||||
)
|
||||
end
|
||||
|
||||
def list_steps(%{id: pipeline_id, visibility: visibility}, _invalid_user)
|
||||
when visibility in [:unlisted, :public] do
|
||||
Repo.all(
|
||||
from s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
order_by: s.position
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Preloads the `:steps` field on a Memex.Pipelines.Pipeline
|
||||
"""
|
||||
@spec preload_steps(Pipeline.t(), User.t()) :: Pipeline.t()
|
||||
def preload_steps(pipeline, user) do
|
||||
%{pipeline | steps: list_steps(pipeline, user)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single step.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Step does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_step!(123, %User{id: 123})
|
||||
%Step{}
|
||||
|
||||
iex> get_step!(456, %User{id: 123})
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
@spec get_step!(Step.id(), User.t()) :: Step.t()
|
||||
def get_step!(id, %{id: user_id}) do
|
||||
Repo.one!(from n in Step, where: n.id == ^id, where: n.user_id == ^user_id)
|
||||
end
|
||||
|
||||
def get_step!(id, _invalid_user) do
|
||||
Repo.one!(
|
||||
from n in Step,
|
||||
where: n.id == ^id,
|
||||
where: n.visibility in [:public, :unlisted]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_step(%{field: value}, %User{id: 123})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> create_step(%{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec create_step(position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
||||
{:ok, Step.t()} | {:error, Step.changeset()}
|
||||
@spec create_step(attrs :: map(), position :: non_neg_integer(), Pipeline.t(), User.t()) ::
|
||||
{:ok, Step.t()} | {:error, Step.changeset()}
|
||||
def create_step(attrs \\ %{}, position, pipeline, user) do
|
||||
Step.create_changeset(attrs, position, pipeline, user) |> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_step(step, %{field: new_value}, %User{id: 123})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> update_step(step, %{field: bad_value}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec update_step(Step.t(), attrs :: map(), User.t()) ::
|
||||
{:ok, Step.t()} | {:error, Step.changeset()}
|
||||
def update_step(%Step{} = step, attrs, user) do
|
||||
step
|
||||
|> Step.update_changeset(attrs, user)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_step(%Step{user_id: 123}, %User{id: 123})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(%Step{}, %User{role: :admin})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(%Step{}, %User{id: 123})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
@spec delete_step(Step.t(), User.t()) :: {:ok, Step.t()} | {:error, Step.changeset()}
|
||||
def delete_step(%Step{user_id: user_id} = step, %{id: user_id}) do
|
||||
delete_step(step)
|
||||
end
|
||||
|
||||
def delete_step(%Step{} = step, %{role: :admin}) do
|
||||
delete_step(step)
|
||||
end
|
||||
|
||||
defp delete_step(step) do
|
||||
Multi.new()
|
||||
|> Multi.delete(:delete_step, step)
|
||||
|> Multi.update_all(
|
||||
:reorder_steps,
|
||||
fn %{delete_step: %{position: position, pipeline_id: pipeline_id}} ->
|
||||
from s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
where: s.position > ^position,
|
||||
update: [set: [position: s.position - 1]]
|
||||
end,
|
||||
[]
|
||||
)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{delete_step: step}} -> {:ok, step}
|
||||
{:error, :delete_step, changeset, _changes_so_far} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking step changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_step(step, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Step{}}
|
||||
|
||||
iex> change_step(step, %{title: "new title"}, %User{id: 123})
|
||||
%Ecto.Changeset{data: %Step{}}
|
||||
|
||||
"""
|
||||
@spec change_step(Step.t(), User.t()) :: Step.changeset()
|
||||
@spec change_step(Step.t(), attrs :: map(), User.t()) :: Step.changeset()
|
||||
def change_step(%Step{} = step, attrs \\ %{}, user) do
|
||||
step |> Step.update_changeset(attrs, user)
|
||||
end
|
||||
|
||||
@spec reorder_step(Step.t(), :up | :down, User.t()) ::
|
||||
{:ok, Step.t()} | {:error, Step.changeset()}
|
||||
def reorder_step(%Step{position: 0} = step, :up, _user), do: {:error, step}
|
||||
|
||||
def reorder_step(
|
||||
%Step{position: position, pipeline_id: pipeline_id, user_id: user_id} = step,
|
||||
:up,
|
||||
%{id: user_id} = user
|
||||
) do
|
||||
Multi.new()
|
||||
|> Multi.update_all(
|
||||
:reorder_steps,
|
||||
from(s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
where: s.position == ^position - 1,
|
||||
update: [set: [position: ^position]]
|
||||
),
|
||||
[]
|
||||
)
|
||||
|> Multi.update(
|
||||
:update_step,
|
||||
step |> Step.position_changeset(position - 1, user)
|
||||
)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{update_step: step}} -> {:ok, step}
|
||||
{:error, :update_step, changeset, _changes_so_far} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
def reorder_step(
|
||||
%Step{pipeline_id: pipeline_id, position: position, user_id: user_id} = step,
|
||||
:down,
|
||||
%{id: user_id} = user
|
||||
) do
|
||||
Multi.new()
|
||||
|> Multi.one(
|
||||
:step_count,
|
||||
from(s in Step, where: s.pipeline_id == ^pipeline_id, distinct: true, select: count(s.id))
|
||||
)
|
||||
|> Multi.update_all(
|
||||
:reorder_steps,
|
||||
from(s in Step,
|
||||
where: s.pipeline_id == ^pipeline_id,
|
||||
where: s.position == ^position + 1,
|
||||
update: [set: [position: ^position]]
|
||||
),
|
||||
[]
|
||||
)
|
||||
|> Multi.update(:update_step, fn %{step_count: step_count} ->
|
||||
new_position = if position >= step_count - 1, do: position, else: position + 1
|
||||
step |> Step.position_changeset(new_position, user)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{update_step: step}} -> {:ok, step}
|
||||
{:error, :update_step, changeset, _changes_so_far} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
end
|
@ -1,104 +0,0 @@
|
||||
defmodule Memex.Steps do
|
||||
@moduledoc """
|
||||
The Steps context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Memex.Repo
|
||||
|
||||
alias Memex.Steps.Step
|
||||
|
||||
@doc """
|
||||
Returns the list of steps.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_steps()
|
||||
[%Step{}, ...]
|
||||
|
||||
"""
|
||||
def list_steps do
|
||||
Repo.all(Step)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single step.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Step does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_step!(123)
|
||||
%Step{}
|
||||
|
||||
iex> get_step!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_step!(id), do: Repo.get!(Step, id)
|
||||
|
||||
@doc """
|
||||
Creates a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_step(%{field: value})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> create_step(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_step(attrs \\ %{}) do
|
||||
%Step{}
|
||||
|> Step.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_step(step, %{field: new_value})
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> update_step(step, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_step(%Step{} = step, attrs) do
|
||||
step
|
||||
|> Step.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_step(step)
|
||||
{:ok, %Step{}}
|
||||
|
||||
iex> delete_step(step)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def delete_step(%Step{} = step) do
|
||||
Repo.delete(step)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking step changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_step(step)
|
||||
%Ecto.Changeset{data: %Step{}}
|
||||
|
||||
"""
|
||||
def change_step(%Step{} = step, attrs \\ %{}) do
|
||||
Step.changeset(step, attrs)
|
||||
end
|
||||
end
|
@ -1,22 +0,0 @@
|
||||
defmodule Memex.Steps.Step do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "steps" do
|
||||
field :description, :string
|
||||
field :position, :integer
|
||||
field :title, :string
|
||||
field :pipeline_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(step, attrs) do
|
||||
step
|
||||
|> cast(attrs, [:title, :description, :position])
|
||||
|> validate_required([:title, :description, :position])
|
||||
end
|
||||
end
|
@ -1,20 +0,0 @@
|
||||
defmodule Memex.Steps.StepContext do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
schema "step_contexts" do
|
||||
field :step_id, :binary_id
|
||||
field :context_id, :binary_id
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(step_context, attrs) do
|
||||
step_context
|
||||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
end
|
||||
end
|
44
lib/memex_web/components/context_content.ex
Normal file
44
lib/memex_web/components/context_content.ex
Normal file
@ -0,0 +1,44 @@
|
||||
defmodule MemexWeb.Components.ContextContent do
|
||||
@moduledoc """
|
||||
Display the content for a context
|
||||
"""
|
||||
use MemexWeb, :component
|
||||
alias Memex.Contexts.Context
|
||||
alias Phoenix.HTML
|
||||
|
||||
attr :context, Context, required: true
|
||||
|
||||
def context_content(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"show-context-content-#{@context.id}"}
|
||||
class="input input-primary h-128 min-h-128 inline-block"
|
||||
phx-hook="MaintainAttrs"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= add_links_to_content(@context.content) %></p></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp add_links_to_content(content) do
|
||||
Regex.replace(
|
||||
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
|
||||
content,
|
||||
fn _whole_match, slug ->
|
||||
link =
|
||||
HTML.Link.link(
|
||||
"[[#{slug}]]",
|
||||
to: Routes.note_show_path(Endpoint, :show, slug),
|
||||
class: "link inline",
|
||||
data: [qa: "context-note-#{slug}"]
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
"</p>#{link}<p class=\"inline\">"
|
||||
end
|
||||
)
|
||||
|> HTML.raw()
|
||||
end
|
||||
end
|
135
lib/memex_web/components/contexts_table_component.ex
Normal file
135
lib/memex_web/components/contexts_table_component.ex
Normal file
@ -0,0 +1,135 @@
|
||||
defmodule MemexWeb.Components.ContextsTableComponent do
|
||||
@moduledoc """
|
||||
A component that displays a list of contexts
|
||||
"""
|
||||
use MemexWeb, :live_component
|
||||
alias Ecto.UUID
|
||||
alias Memex.{Accounts.User, Contexts, Contexts.Context}
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@impl true
|
||||
@spec update(
|
||||
%{
|
||||
required(:id) => UUID.t(),
|
||||
required(:current_user) => User.t(),
|
||||
required(:contexts) => [Context.t()],
|
||||
optional(any()) => any()
|
||||
},
|
||||
Socket.t()
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{id: _id, contexts: _contexts, current_user: _current_user} = assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:actions, fn -> [] end)
|
||||
|> display_contexts()
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp display_contexts(
|
||||
%{
|
||||
assigns: %{
|
||||
contexts: contexts,
|
||||
current_user: current_user,
|
||||
actions: actions
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] or current_user |> is_nil() do
|
||||
[]
|
||||
else
|
||||
[%{label: nil, key: :actions, sortable: false}]
|
||||
end
|
||||
|
||||
columns = [
|
||||
%{label: gettext("slug"), key: :slug},
|
||||
%{label: gettext("content"), key: :content},
|
||||
%{label: gettext("tags"), key: :tags},
|
||||
%{label: gettext("visibility"), key: :visibility}
|
||||
| columns
|
||||
]
|
||||
|
||||
rows =
|
||||
contexts
|
||||
|> Enum.map(fn context ->
|
||||
context
|
||||
|> get_row_data_for_context(%{
|
||||
columns: columns,
|
||||
current_user: current_user,
|
||||
actions: actions
|
||||
})
|
||||
end)
|
||||
|
||||
socket |> assign(columns: columns, rows: rows)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="w-full">
|
||||
<.live_component
|
||||
module={MemexWeb.Components.TableComponent}
|
||||
id={@id}
|
||||
columns={@columns}
|
||||
rows={@rows}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@spec get_row_data_for_context(Context.t(), additional_data :: map()) :: map()
|
||||
defp get_row_data_for_context(context, %{columns: columns} = additional_data) do
|
||||
columns
|
||||
|> Map.new(fn %{key: key} ->
|
||||
{key, get_value_for_key(key, context, additional_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec get_value_for_key(atom(), Context.t(), additional_data :: map()) ::
|
||||
any() | {any(), Rendered.t()}
|
||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
||||
assigns = %{slug: slug}
|
||||
|
||||
slug_block = ~H"""
|
||||
<.link
|
||||
navigate={Routes.context_show_path(Endpoint, :show, @slug)}
|
||||
class="link"
|
||||
data-qa={"context-show-#{@slug}"}
|
||||
>
|
||||
<%= @slug %>
|
||||
</.link>
|
||||
"""
|
||||
|
||||
{slug, slug_block}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:content, %{content: content}, _additional_data) do
|
||||
assigns = %{content: content}
|
||||
|
||||
content_block = ~H"""
|
||||
<div class="truncate max-w-sm">
|
||||
<%= @content %>
|
||||
</div>
|
||||
"""
|
||||
|
||||
{content, content_block}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
|
||||
tags |> Contexts.get_tags_string()
|
||||
end
|
||||
|
||||
defp get_value_for_key(:actions, context, %{actions: actions}) do
|
||||
assigns = %{actions: actions, context: context}
|
||||
|
||||
~H"""
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<%= render_slot(@actions, @context) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_value_for_key(key, context, _additional_data), do: context |> Map.get(key)
|
||||
end
|
@ -1,29 +0,0 @@
|
||||
defmodule MemexWeb.Components.NoteCard do
|
||||
@moduledoc """
|
||||
Display card for an note
|
||||
"""
|
||||
|
||||
use MemexWeb, :component
|
||||
|
||||
def note_card(assigns) do
|
||||
~H"""
|
||||
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out">
|
||||
<h1 class="title text-xl">
|
||||
<%= @note.name %>
|
||||
</h1>
|
||||
|
||||
<h2 class="title text-md">
|
||||
<%= gettext("visibility: %{visibility}", visibility: @note.visibility) %>
|
||||
</h2>
|
||||
|
||||
<%= if @inner_block do %>
|
||||
<div class="flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
@ -44,7 +44,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
|
||||
end
|
||||
|
||||
columns = [
|
||||
%{label: gettext("title"), key: :title},
|
||||
%{label: gettext("slug"), key: :slug},
|
||||
%{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(:title, %{id: id, title: title}, _additional_data) do
|
||||
assigns = %{id: id, title: title}
|
||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
||||
assigns = %{slug: slug}
|
||||
|
||||
title_block = ~H"""
|
||||
slug_block = ~H"""
|
||||
<.link
|
||||
navigate={Routes.note_show_path(Endpoint, :show, @id)}
|
||||
navigate={Routes.note_show_path(Endpoint, :show, @slug)}
|
||||
class="link"
|
||||
data-qa={"note-show-#{@id}"}
|
||||
data-qa={"note-show-#{@slug}"}
|
||||
>
|
||||
<%= @title %>
|
||||
<%= @slug %>
|
||||
</.link>
|
||||
"""
|
||||
|
||||
{title, title_block}
|
||||
{slug, slug_block}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:content, %{content: content}, _additional_data) do
|
||||
|
135
lib/memex_web/components/pipelines_table_component.ex
Normal file
135
lib/memex_web/components/pipelines_table_component.ex
Normal file
@ -0,0 +1,135 @@
|
||||
defmodule MemexWeb.Components.PipelinesTableComponent do
|
||||
@moduledoc """
|
||||
A component that displays a list of pipelines
|
||||
"""
|
||||
use MemexWeb, :live_component
|
||||
alias Ecto.UUID
|
||||
alias Memex.{Accounts.User, Pipelines, Pipelines.Pipeline}
|
||||
alias Phoenix.LiveView.{Rendered, Socket}
|
||||
|
||||
@impl true
|
||||
@spec update(
|
||||
%{
|
||||
required(:id) => UUID.t(),
|
||||
required(:current_user) => User.t(),
|
||||
required(:pipelines) => [Pipeline.t()],
|
||||
optional(any()) => any()
|
||||
},
|
||||
Socket.t()
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{id: _id, pipelines: _pipelines, current_user: _current_user} = assigns, socket) do
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_new(:actions, fn -> [] end)
|
||||
|> display_pipelines()
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp display_pipelines(
|
||||
%{
|
||||
assigns: %{
|
||||
pipelines: pipelines,
|
||||
current_user: current_user,
|
||||
actions: actions
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
columns =
|
||||
if actions == [] or current_user |> is_nil() do
|
||||
[]
|
||||
else
|
||||
[%{label: nil, key: :actions, sortable: false}]
|
||||
end
|
||||
|
||||
columns = [
|
||||
%{label: gettext("slug"), key: :slug},
|
||||
%{label: gettext("description"), key: :description},
|
||||
%{label: gettext("tags"), key: :tags},
|
||||
%{label: gettext("visibility"), key: :visibility}
|
||||
| columns
|
||||
]
|
||||
|
||||
rows =
|
||||
pipelines
|
||||
|> Enum.map(fn pipeline ->
|
||||
pipeline
|
||||
|> get_row_data_for_pipeline(%{
|
||||
columns: columns,
|
||||
current_user: current_user,
|
||||
actions: actions
|
||||
})
|
||||
end)
|
||||
|
||||
socket |> assign(columns: columns, rows: rows)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="w-full">
|
||||
<.live_component
|
||||
module={MemexWeb.Components.TableComponent}
|
||||
id={@id}
|
||||
columns={@columns}
|
||||
rows={@rows}
|
||||
/>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@spec get_row_data_for_pipeline(Pipeline.t(), additional_data :: map()) :: map()
|
||||
defp get_row_data_for_pipeline(pipeline, %{columns: columns} = additional_data) do
|
||||
columns
|
||||
|> Map.new(fn %{key: key} ->
|
||||
{key, get_value_for_key(key, pipeline, additional_data)}
|
||||
end)
|
||||
end
|
||||
|
||||
@spec get_value_for_key(atom(), Pipeline.t(), additional_data :: map()) ::
|
||||
any() | {any(), Rendered.t()}
|
||||
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do
|
||||
assigns = %{slug: slug}
|
||||
|
||||
slug_block = ~H"""
|
||||
<.link
|
||||
navigate={Routes.pipeline_show_path(Endpoint, :show, @slug)}
|
||||
class="link"
|
||||
data-qa={"pipeline-show-#{@slug}"}
|
||||
>
|
||||
<%= @slug %>
|
||||
</.link>
|
||||
"""
|
||||
|
||||
{slug, slug_block}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:description, %{description: description}, _additional_data) do
|
||||
assigns = %{description: description}
|
||||
|
||||
description_block = ~H"""
|
||||
<div class="truncate max-w-sm">
|
||||
<%= @description %>
|
||||
</div>
|
||||
"""
|
||||
|
||||
{description, description_block}
|
||||
end
|
||||
|
||||
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
|
||||
tags |> Pipelines.get_tags_string()
|
||||
end
|
||||
|
||||
defp get_value_for_key(:actions, pipeline, %{actions: actions}) do
|
||||
assigns = %{actions: actions, pipeline: pipeline}
|
||||
|
||||
~H"""
|
||||
<div class="flex justify-center items-center space-x-4">
|
||||
<%= render_slot(@actions, @pipeline) %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_value_for_key(key, pipeline, _additional_data), do: pipeline |> Map.get(key)
|
||||
end
|
44
lib/memex_web/components/step_content.ex
Normal file
44
lib/memex_web/components/step_content.ex
Normal file
@ -0,0 +1,44 @@
|
||||
defmodule MemexWeb.Components.StepContent do
|
||||
@moduledoc """
|
||||
Display the content for a step
|
||||
"""
|
||||
use MemexWeb, :component
|
||||
alias Memex.Pipelines.Steps.Step
|
||||
alias Phoenix.HTML
|
||||
|
||||
attr :step, Step, required: true
|
||||
|
||||
def step_content(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"show-step-content-#{@step.id}"}
|
||||
class="input input-primary h-32 min-h-32 inline-block"
|
||||
phx-hook="MaintainAttrs"
|
||||
phx-update="ignore"
|
||||
readonly
|
||||
phx-no-format
|
||||
><p class="inline"><%= add_links_to_content(@step.content) %></p></div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp add_links_to_content(content) do
|
||||
Regex.replace(
|
||||
~r/\[\[([\p{L}\p{N}\-]+)\]\]/,
|
||||
content,
|
||||
fn _whole_match, slug ->
|
||||
link =
|
||||
HTML.Link.link(
|
||||
"[[#{slug}]]",
|
||||
to: Routes.context_show_path(Endpoint, :show, slug),
|
||||
class: "link inline",
|
||||
data: [qa: "step-context-#{slug}"]
|
||||
)
|
||||
|> HTML.Safe.to_iodata()
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
"</p>#{link}<p class=\"inline\">"
|
||||
end
|
||||
)
|
||||
|> HTML.raw()
|
||||
end
|
||||
end
|
@ -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.role == :admin do %>
|
||||
<%= if @current_user |> Accounts.is_already_admin?() do %>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.invite_index_path(Endpoint, :index)}
|
||||
|
@ -2,7 +2,6 @@ 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
|
||||
@ -30,7 +29,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(%User{}),
|
||||
changeset: Accounts.change_user_registration(),
|
||||
invite: invite,
|
||||
page_title: gettext("register")
|
||||
)
|
||||
|
@ -4,8 +4,8 @@ defmodule MemexWeb.ContextLive.FormComponent do
|
||||
alias Memex.Contexts
|
||||
|
||||
@impl true
|
||||
def update(%{context: context} = assigns, socket) do
|
||||
changeset = Contexts.change_context(context)
|
||||
def update(%{context: context, current_user: current_user} = assigns, socket) do
|
||||
changeset = Contexts.change_context(context, current_user)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
@ -14,39 +14,52 @@ defmodule MemexWeb.ContextLive.FormComponent do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"context" => context_params}, socket) do
|
||||
def handle_event(
|
||||
"validate",
|
||||
%{"context" => context_params},
|
||||
%{assigns: %{context: context, current_user: current_user}} = socket
|
||||
) do
|
||||
changeset =
|
||||
socket.assigns.context
|
||||
|> Contexts.change_context(context_params)
|
||||
context
|
||||
|> Contexts.change_context(context_params, current_user)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"context" => context_params}, socket) do
|
||||
save_context(socket, socket.assigns.action, context_params)
|
||||
def handle_event("save", %{"context" => context_params}, %{assigns: %{action: action}} = socket) do
|
||||
save_context(socket, action, context_params)
|
||||
end
|
||||
|
||||
defp save_context(socket, :edit, context_params) do
|
||||
case Contexts.update_context(socket.assigns.context, context_params) do
|
||||
{:ok, _context} ->
|
||||
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}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "context updated successfully")
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|> put_flash(:info, gettext("%{slug} saved", slug: slug))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_context(socket, :new, context_params) do
|
||||
case Contexts.create_context(context_params) do
|
||||
{:ok, _context} ->
|
||||
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}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "context created successfully")
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|> put_flash(:info, gettext("%{slug} created", slug: slug))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
|
@ -1,6 +1,4 @@
|
||||
<div>
|
||||
<h2><%= @title %></h2>
|
||||
|
||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
@ -8,27 +6,44 @@
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-debounce="300"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= label(f, :title) %>
|
||||
<%= text_input(f, :title) %>
|
||||
<%= error_tag(f, :title) %>
|
||||
<%= text_input(f, :slug,
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("slug")
|
||||
) %>
|
||||
<%= error_tag(f, :slug) %>
|
||||
|
||||
<%= label(f, :content) %>
|
||||
<%= textarea(f, :content) %>
|
||||
<%= textarea(f, :content,
|
||||
id: "context-form-content",
|
||||
class: "input input-primary h-64 min-h-64",
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore",
|
||||
placeholder: gettext("use [[note-slug]] to link to a note")
|
||||
) %>
|
||||
<%= error_tag(f, :content) %>
|
||||
|
||||
<%= 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"
|
||||
<%= 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)
|
||||
) %>
|
||||
<%= error_tag(f, :visibility) %>
|
||||
<%= error_tag(f, :tags_string) %>
|
||||
|
||||
<div>
|
||||
<%= submit("Save", phx_disable_with: "Saving...") %>
|
||||
<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) %>
|
||||
</.form>
|
||||
</div>
|
||||
|
@ -1,46 +1,89 @@
|
||||
defmodule MemexWeb.ContextLive.Index do
|
||||
use MemexWeb, :live_view
|
||||
|
||||
alias Memex.Contexts
|
||||
alias Memex.Contexts.Context
|
||||
alias Memex.{Accounts.User, Contexts, 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, assign(socket, :contexts, list_contexts())}
|
||||
{:ok, socket |> assign(search: nil) |> display_contexts()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
|
||||
{:noreply, apply_action(socket, live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
|
||||
%{slug: slug} = context = Contexts.get_context_by_slug(slug, current_user)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "edit context")
|
||||
|> assign(:context, Contexts.get_context!(id))
|
||||
|> assign(page_title: gettext("edit %{slug}", slug: slug))
|
||||
|> assign(context: context)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "new context")
|
||||
|> assign(:context, %Context{})
|
||||
|> assign(page_title: gettext("new context"))
|
||||
|> assign(context: %Context{visibility: :private, user_id: current_user_id})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "listing contexts")
|
||||
|> assign(:context, nil)
|
||||
|> 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()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
context = Contexts.get_context!(id)
|
||||
{:ok, _} = Contexts.delete_context(context)
|
||||
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)
|
||||
|
||||
{:noreply, assign(socket, :contexts, list_contexts())}
|
||||
socket =
|
||||
socket
|
||||
|> assign(contexts: Contexts.list_contexts(current_user))
|
||||
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp list_contexts do
|
||||
Contexts.list_contexts()
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.context_index_path(Endpoint, :index))}
|
||||
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
|
||||
|
@ -1,10 +1,71 @@
|
||||
<h1>listing contexts</h1>
|
||||
<div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= gettext("contexts") %>
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={:search}
|
||||
phx-change="search"
|
||||
phx-submit="search"
|
||||
class="self-stretch flex flex-col items-stretch"
|
||||
>
|
||||
<%= text_input(f, :search_term,
|
||||
class: "input input-primary",
|
||||
value: @search,
|
||||
phx_debounce: 300,
|
||||
placeholder: gettext("search")
|
||||
) %>
|
||||
</.form>
|
||||
|
||||
<%= if @contexts |> Enum.empty?() do %>
|
||||
<h1 class="self-center text-primary-500">
|
||||
<%= gettext("no contexts found") %>
|
||||
</h1>
|
||||
<% else %>
|
||||
<.live_component
|
||||
module={MemexWeb.Components.ContextsTableComponent}
|
||||
id="contexts-index-table"
|
||||
current_user={@current_user}
|
||||
contexts={@contexts}
|
||||
>
|
||||
<:actions :let={context}>
|
||||
<%= if is_owner?(context, @current_user) do %>
|
||||
<.link
|
||||
patch={Routes.context_index_path(@socket, :edit, context.slug)}
|
||||
data-qa={"context-edit-#{context.id}"}
|
||||
>
|
||||
<%= dgettext("actions", "edit") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
<%= if is_owner_or_admin?(context, @current_user) do %>
|
||||
<.link
|
||||
href="#"
|
||||
phx-click="delete"
|
||||
phx-value-id={context.id}
|
||||
data-confirm={dgettext("prompts", "are you sure?")}
|
||||
data-qa={"delete-context-#{context.id}"}
|
||||
>
|
||||
<%= dgettext("actions", "delete") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</:actions>
|
||||
</.live_component>
|
||||
<% end %>
|
||||
|
||||
<%= if @current_user do %>
|
||||
<.link patch={Routes.context_index_path(@socket, :new)} class="self-end btn btn-primary">
|
||||
<%= dgettext("actions", "new context") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if @live_action in [:new, :edit] do %>
|
||||
<.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}
|
||||
@ -12,55 +73,3 @@
|
||||
/>
|
||||
</.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>
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule MemexWeb.ContextLive.Show do
|
||||
use MemexWeb, :live_view
|
||||
|
||||
alias Memex.Contexts
|
||||
import MemexWeb.Components.ContextContent
|
||||
alias Memex.{Accounts.User, Contexts, Contexts.Context}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@ -9,13 +9,50 @@ defmodule MemexWeb.ContextLive.Show do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:context, Contexts.get_context!(id))}
|
||||
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}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "show context"
|
||||
defp page_title(:edit), do: "edit context"
|
||||
@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
|
||||
end
|
||||
|
@ -1,48 +1,52 @@
|
||||
<h1>show context</h1>
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= @context.slug %>
|
||||
</h1>
|
||||
|
||||
<p><%= if @context.tags, do: @context.tags |> Enum.join(", ") %></p>
|
||||
|
||||
<.context_content context={@context} />
|
||||
|
||||
<p class="self-end">
|
||||
<%= gettext("Visibility: %{visibility}", visibility: @context.visibility) %>
|
||||
</p>
|
||||
|
||||
<div class="self-end flex space-x-4">
|
||||
<.link class="btn btn-primary" navigate={Routes.context_index_path(@socket, :index)}>
|
||||
<%= dgettext("actions", "back") %>
|
||||
</.link>
|
||||
<%= if is_owner?(@context, @current_user) do %>
|
||||
<.link
|
||||
class="btn btn-primary"
|
||||
patch={Routes.context_show_path(@socket, :edit, @context.slug)}
|
||||
>
|
||||
<%= dgettext("actions", "edit") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
<%= if is_owner_or_admin?(@context, @current_user) do %>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
phx-click="delete"
|
||||
data-confirm={dgettext("prompts", "are you sure?")}
|
||||
data-qa={"delete-context-#{@context.id}"}
|
||||
>
|
||||
<%= dgettext("actions", "delete") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @live_action in [:edit] do %>
|
||||
<.modal return_to={Routes.context_show_path(@socket, :show, @context)}>
|
||||
<.modal return_to={Routes.context_show_path(@socket, :show, @context.slug)}>
|
||||
<.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)}
|
||||
return_to={Routes.context_show_path(@socket, :show, @context.slug)}
|
||||
/>
|
||||
</.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>
|
||||
|
12
lib/memex_web/live/faq_live.ex
Normal file
12
lib/memex_web/live/faq_live.ex
Normal file
@ -0,0 +1,12 @@
|
||||
defmodule MemexWeb.FaqLive do
|
||||
@moduledoc """
|
||||
Liveview for the faq page
|
||||
"""
|
||||
|
||||
use MemexWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket |> assign(page_title: gettext("faq"))}
|
||||
end
|
||||
end
|
124
lib/memex_web/live/faq_live.html.heex
Normal file
124
lib/memex_web/live/faq_live.html.heex
Normal file
@ -0,0 +1,124 @@
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-8 text-center max-w-3xl">
|
||||
<h1 class="title text-primary-400 text-2xl">
|
||||
<%= gettext("faq") %>
|
||||
</h1>
|
||||
|
||||
<hr class="hr" />
|
||||
|
||||
<ul class="flex flex-col justify-center items-stretch space-y-8">
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("what is this?") %>
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"this is a memex, used to document not just your notes, but also your perspectives and processes."
|
||||
) %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= gettext("some things that this memex is very loosely inspired by:") %>
|
||||
</p>
|
||||
|
||||
<ul class="list-disc flex flex-col justify-center items-center space-y-2">
|
||||
<li>
|
||||
<.link
|
||||
href="https://en.wikipedia.org/wiki/Memex"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("memex") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
href="https://en.wikipedia.org/wiki/Zettelkasten"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("zettelkasten") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
href="https://en.wikipedia.org/wiki/Org-mode"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<%= gettext("org-mode") %>
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("why split up into notes, contexts and pipelines?") %>
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
|
||||
) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
|
||||
) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
|
||||
) %>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("what should my notes be like?") %>
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
|
||||
) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"spoons? probably not. a particular brand of spoons that you really like? why not :)"
|
||||
) %>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("what should my contexts be like?") %>
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext("in my opinion, contexts should be like single-topic blog posts.") %>
|
||||
</p>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
|
||||
) %>
|
||||
</p>
|
||||
</li>
|
||||
|
||||
<li class="flex flex-col justify-center items-stretch space-y-2">
|
||||
<b class="whitespace-nowrap">
|
||||
<%= gettext("what should my pipelines be like?") %>
|
||||
</b>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
|
||||
) %>
|
||||
</p>
|
||||
<p>
|
||||
<%= gettext(
|
||||
"for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
|
||||
) %>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
@ -5,41 +5,11 @@ 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"), 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}
|
||||
{:ok, socket |> assign(page_title: gettext("home"), admins: admins)}
|
||||
end
|
||||
end
|
||||
|
@ -1,13 +1,12 @@
|
||||
<div class="flex flex-col justify-center items-center text-center space-y-4">
|
||||
<h1 class="title text-primary-400 text-2xl">
|
||||
<%= gettext("memex") %>
|
||||
<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") %>
|
||||
</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>
|
||||
@ -16,8 +15,7 @@
|
||||
</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>
|
||||
@ -26,8 +24,7 @@
|
||||
</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>
|
||||
@ -35,6 +32,15 @@
|
||||
<%= 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" />
|
||||
@ -44,8 +50,7 @@
|
||||
<%= 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>
|
||||
@ -54,8 +59,7 @@
|
||||
</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>
|
||||
@ -64,8 +68,7 @@
|
||||
</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>
|
||||
@ -88,16 +91,13 @@
|
||||
</b>
|
||||
<p>
|
||||
<%= if @admins |> Enum.empty?() do %>
|
||||
<.link
|
||||
href={Routes.user_registration_path(MemexWeb.Endpoint, :new)}
|
||||
class="hover:underline"
|
||||
>
|
||||
<%= dgettext("prompts", "register to setup %{name}", name: "memex") %>
|
||||
<.link href={Routes.user_registration_path(Endpoint, :new)} class="link">
|
||||
<%= 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="hover:underline" href={"mailto:#{admin.email}"}>
|
||||
<a class="link" 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, MemexWeb.Endpoint)[:registration]
|
||||
<%= Application.get_env(:memex, 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 hover:underline"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
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 hover:underline"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
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 hover:underline"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
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 hover:underline"
|
||||
class="flex flex-row justify-center items-center space-x-2 link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
@ -37,10 +37,10 @@ defmodule MemexWeb.NoteLive.FormComponent do
|
||||
note_params
|
||||
) do
|
||||
case Notes.update_note(note, note_params, current_user) do
|
||||
{:ok, %{title: title}} ->
|
||||
{:ok, %{slug: slug}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, gettext("%{title} saved", title: title))
|
||||
|> put_flash(:info, gettext("%{slug} saved", slug: slug))
|
||||
|> 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, %{title: title}} ->
|
||||
{:ok, %{slug: slug}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, gettext("%{title} created", title: title))
|
||||
|> put_flash(:info, gettext("%{slug} created", slug: slug))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
|
@ -9,11 +9,11 @@
|
||||
phx-debounce="300"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= text_input(f, :title,
|
||||
<%= text_input(f, :slug,
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("title")
|
||||
placeholder: gettext("slug")
|
||||
) %>
|
||||
<%= error_tag(f, :title) %>
|
||||
<%= error_tag(f, :slug) %>
|
||||
|
||||
<%= textarea(f, :content,
|
||||
id: "note-form-content",
|
||||
|
@ -1,6 +1,6 @@
|
||||
defmodule MemexWeb.NoteLive.Index do
|
||||
use MemexWeb, :live_view
|
||||
alias Memex.{Notes, Notes.Note}
|
||||
alias Memex.{Accounts.User, 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, %{"id" => id}) do
|
||||
%{title: title} = note = Notes.get_note!(id, current_user)
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
|
||||
%{slug: slug} = note = Notes.get_note_by_slug(slug, current_user)
|
||||
|
||||
socket
|
||||
|> assign(page_title: gettext("edit %{title}", title: title))
|
||||
|> assign(page_title: gettext("edit %{slug}", slug: slug))
|
||||
|> assign(note: note)
|
||||
end
|
||||
|
||||
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
||||
socket
|
||||
|> assign(page_title: "new note")
|
||||
|> assign(note: %Note{user_id: current_user_id})
|
||||
|> assign(page_title: gettext("new note"))
|
||||
|> assign(note: %Note{visibility: :private, user_id: current_user_id})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(page_title: "notes")
|
||||
|> assign(page_title: gettext("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: "notes")
|
||||
|> assign(page_title: gettext("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
|
||||
%{title: title} = note = Notes.get_note!(id, current_user)
|
||||
{:ok, _} = Notes.delete_note(note, current_user)
|
||||
note = Notes.get_note!(id, current_user)
|
||||
{:ok, %{slug: slug}} = Notes.delete_note(note, current_user)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(notes: Notes.list_notes(current_user))
|
||||
|> put_flash(:info, gettext("%{title} deleted", title: title))
|
||||
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
@ -76,4 +76,13 @@ 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
|
||||
|
@ -8,10 +8,14 @@
|
||||
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) %>
|
||||
<%= text_input(f, :search_term,
|
||||
class: "input input-primary",
|
||||
value: @search,
|
||||
phx_debounce: 300,
|
||||
placeholder: gettext("search")
|
||||
) %>
|
||||
</.form>
|
||||
|
||||
<%= if @notes |> Enum.empty?() do %>
|
||||
@ -26,13 +30,15 @@
|
||||
notes={@notes}
|
||||
>
|
||||
<:actions :let={note}>
|
||||
<%= if @current_user do %>
|
||||
<%= if is_owner?(note, @current_user) do %>
|
||||
<.link
|
||||
patch={Routes.note_index_path(@socket, :edit, note)}
|
||||
patch={Routes.note_index_path(@socket, :edit, note.slug)}
|
||||
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"
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule MemexWeb.NoteLive.Show do
|
||||
use MemexWeb, :live_view
|
||||
|
||||
alias Memex.Notes
|
||||
alias Memex.{Accounts.User, Notes, Notes.Note}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@ -10,16 +10,49 @@ defmodule MemexWeb.NoteLive.Show do
|
||||
|
||||
@impl true
|
||||
def handle_params(
|
||||
%{"id" => id},
|
||||
%{"slug" => slug},
|
||||
_,
|
||||
%{assigns: %{live_action: live_action, current_user: current_user}} = socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(live_action))
|
||||
|> assign(:note, Notes.get_note!(id, current_user))}
|
||||
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}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "show note"
|
||||
defp page_title(:edit), do: "edit note"
|
||||
@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
|
||||
end
|
||||
|
@ -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.title %>
|
||||
<%= @note.slug %>
|
||||
</h1>
|
||||
|
||||
<p><%= if @note.tags, do: @note.tags |> Enum.join(", ") %></p>
|
||||
@ -19,19 +19,30 @@
|
||||
</p>
|
||||
|
||||
<div class="self-end flex space-x-4">
|
||||
<.link class="btn btn-primary" patch={Routes.note_index_path(@socket, :index)}>
|
||||
<%= dgettext("actions", "Back") %>
|
||||
<.link class="btn btn-primary" navigate={Routes.note_index_path(@socket, :index)}>
|
||||
<%= dgettext("actions", "back") %>
|
||||
</.link>
|
||||
<%= if @current_user do %>
|
||||
<.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note)}>
|
||||
<%= if is_owner?(@note, @current_user) do %>
|
||||
<.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note.slug)}>
|
||||
<%= 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)}>
|
||||
<.modal return_to={Routes.note_show_path(@socket, :show, @note.slug)}>
|
||||
<.live_component
|
||||
module={MemexWeb.NoteLive.FormComponent}
|
||||
id={@note.id}
|
||||
@ -39,7 +50,7 @@
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
note={@note}
|
||||
return_to={Routes.note_show_path(@socket, :show, @note)}
|
||||
return_to={Routes.note_show_path(@socket, :show, @note.slug)}
|
||||
/>
|
||||
</.modal>
|
||||
<% end %>
|
||||
|
@ -4,8 +4,8 @@ defmodule MemexWeb.PipelineLive.FormComponent do
|
||||
alias Memex.Pipelines
|
||||
|
||||
@impl true
|
||||
def update(%{pipeline: pipeline} = assigns, socket) do
|
||||
changeset = Pipelines.change_pipeline(pipeline)
|
||||
def update(%{pipeline: pipeline, current_user: current_user} = assigns, socket) do
|
||||
changeset = Pipelines.change_pipeline(pipeline, current_user)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
@ -14,39 +14,56 @@ defmodule MemexWeb.PipelineLive.FormComponent do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"pipeline" => pipeline_params}, socket) do
|
||||
def handle_event(
|
||||
"validate",
|
||||
%{"pipeline" => pipeline_params},
|
||||
%{assigns: %{pipeline: pipeline, current_user: current_user}} = socket
|
||||
) do
|
||||
changeset =
|
||||
socket.assigns.pipeline
|
||||
|> Pipelines.change_pipeline(pipeline_params)
|
||||
pipeline
|
||||
|> Pipelines.change_pipeline(pipeline_params, current_user)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"pipeline" => pipeline_params}, socket) do
|
||||
save_pipeline(socket, socket.assigns.action, pipeline_params)
|
||||
def handle_event(
|
||||
"save",
|
||||
%{"pipeline" => pipeline_params},
|
||||
%{assigns: %{action: action}} = socket
|
||||
) do
|
||||
save_pipeline(socket, action, pipeline_params)
|
||||
end
|
||||
|
||||
defp save_pipeline(socket, :edit, pipeline_params) do
|
||||
case Pipelines.update_pipeline(socket.assigns.pipeline, pipeline_params) do
|
||||
{:ok, _pipeline} ->
|
||||
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}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "pipeline updated successfully")
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|> put_flash(:info, gettext("%{slug} saved", slug: slug))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_pipeline(socket, :new, pipeline_params) do
|
||||
case Pipelines.create_pipeline(pipeline_params) do
|
||||
{:ok, _pipeline} ->
|
||||
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}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "pipeline created successfully")
|
||||
|> push_navigate(to: socket.assigns.return_to)}
|
||||
|> put_flash(:info, gettext("%{slug} created", slug: slug))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
|
@ -1,6 +1,4 @@
|
||||
<div>
|
||||
<h2><%= @title %></h2>
|
||||
|
||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
@ -8,23 +6,44 @@
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-debounce="300"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= label(f, :title) %>
|
||||
<%= text_input(f, :title) %>
|
||||
<%= error_tag(f, :title) %>
|
||||
<%= text_input(f, :slug,
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("slug")
|
||||
) %>
|
||||
<%= error_tag(f, :slug) %>
|
||||
|
||||
<%= label(f, :description) %>
|
||||
<%= textarea(f, :description) %>
|
||||
<%= textarea(f, :description,
|
||||
id: "pipeline-form-description",
|
||||
class: "input input-primary h-64 min-h-64",
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore",
|
||||
placeholder: gettext("description")
|
||||
) %>
|
||||
<%= error_tag(f, :description) %>
|
||||
|
||||
<%= label(f, :visibility) %>
|
||||
<%= select(f, :visibility, Ecto.Enum.values(Memex.Pipelines.Pipeline, :visibility),
|
||||
prompt: "Choose a value"
|
||||
<%= 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)
|
||||
) %>
|
||||
<%= error_tag(f, :visibility) %>
|
||||
<%= error_tag(f, :tags_string) %>
|
||||
|
||||
<div>
|
||||
<%= submit("Save", phx_disable_with: "Saving...") %>
|
||||
<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) %>
|
||||
</.form>
|
||||
</div>
|
||||
|
@ -1,46 +1,89 @@
|
||||
defmodule MemexWeb.PipelineLive.Index do
|
||||
use MemexWeb, :live_view
|
||||
|
||||
alias Memex.Pipelines
|
||||
alias Memex.Pipelines.Pipeline
|
||||
alias Memex.{Accounts.User, Pipelines, 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, assign(socket, :pipelines, list_pipelines())}
|
||||
{:ok, socket |> assign(search: nil) |> display_pipelines()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
|
||||
{:noreply, apply_action(socket, live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"slug" => slug}) do
|
||||
%{slug: slug} = pipeline = Pipelines.get_pipeline_by_slug(slug, current_user)
|
||||
|
||||
socket
|
||||
|> assign(:page_title, "edit pipeline")
|
||||
|> assign(:pipeline, Pipelines.get_pipeline!(id))
|
||||
|> assign(page_title: gettext("edit %{slug}", slug: slug))
|
||||
|> assign(pipeline: pipeline)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
defp apply_action(%{assigns: %{current_user: %{id: current_user_id}}} = socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "new Pipeline")
|
||||
|> assign(:pipeline, %Pipeline{})
|
||||
|> assign(page_title: gettext("new pipeline"))
|
||||
|> assign(pipeline: %Pipeline{visibility: :private, user_id: current_user_id})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "listing pipelines")
|
||||
|> assign(:pipeline, nil)
|
||||
|> 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()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
pipeline = Pipelines.get_pipeline!(id)
|
||||
{:ok, _} = Pipelines.delete_pipeline(pipeline)
|
||||
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)
|
||||
|
||||
{:noreply, assign(socket, :pipelines, list_pipelines())}
|
||||
socket =
|
||||
socket
|
||||
|> assign(pipelines: Pipelines.list_pipelines(current_user))
|
||||
|> put_flash(:info, gettext("%{slug} deleted", slug: slug))
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp list_pipelines do
|
||||
Pipelines.list_pipelines()
|
||||
@impl true
|
||||
def handle_event("search", %{"search" => %{"search_term" => ""}}, socket) do
|
||||
{:noreply, socket |> push_patch(to: Routes.pipeline_index_path(Endpoint, :index))}
|
||||
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
|
||||
|
@ -1,10 +1,71 @@
|
||||
<h1>listing pipelines</h1>
|
||||
<div class="mx-auto flex flex-col justify-center items-start space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= gettext("pipelines") %>
|
||||
</h1>
|
||||
|
||||
<.form
|
||||
:let={f}
|
||||
for={:search}
|
||||
phx-change="search"
|
||||
phx-submit="search"
|
||||
class="self-stretch flex flex-col items-stretch"
|
||||
>
|
||||
<%= text_input(f, :search_term,
|
||||
class: "input input-primary",
|
||||
value: @search,
|
||||
phx_debounce: 300,
|
||||
placeholder: gettext("search")
|
||||
) %>
|
||||
</.form>
|
||||
|
||||
<%= if @pipelines |> Enum.empty?() do %>
|
||||
<h1 class="self-center text-primary-500">
|
||||
<%= gettext("no pipelines found") %>
|
||||
</h1>
|
||||
<% else %>
|
||||
<.live_component
|
||||
module={MemexWeb.Components.PipelinesTableComponent}
|
||||
id="pipelines-index-table"
|
||||
current_user={@current_user}
|
||||
pipelines={@pipelines}
|
||||
>
|
||||
<:actions :let={pipeline}>
|
||||
<%= if is_owner?(pipeline, @current_user) do %>
|
||||
<.link
|
||||
patch={Routes.pipeline_index_path(@socket, :edit, pipeline.slug)}
|
||||
data-qa={"pipeline-edit-#{pipeline.id}"}
|
||||
>
|
||||
<%= dgettext("actions", "edit") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
<%= if is_owner_or_admin?(pipeline, @current_user) do %>
|
||||
<.link
|
||||
href="#"
|
||||
phx-click="delete"
|
||||
phx-value-id={pipeline.id}
|
||||
data-confirm={dgettext("prompts", "are you sure?")}
|
||||
data-qa={"delete-pipeline-#{pipeline.id}"}
|
||||
>
|
||||
<%= dgettext("actions", "delete") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</:actions>
|
||||
</.live_component>
|
||||
<% end %>
|
||||
|
||||
<%= if @current_user do %>
|
||||
<.link patch={Routes.pipeline_index_path(@socket, :new)} class="self-end btn btn-primary">
|
||||
<%= dgettext("actions", "new pipeline") %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if @live_action in [:new, :edit] do %>
|
||||
<.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}
|
||||
@ -12,53 +73,3 @@
|
||||
/>
|
||||
</.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>
|
||||
|
@ -1,7 +1,8 @@
|
||||
defmodule MemexWeb.PipelineLive.Show do
|
||||
use MemexWeb, :live_view
|
||||
|
||||
alias Memex.Pipelines
|
||||
import MemexWeb.Components.StepContent
|
||||
alias Memex.{Accounts.User, Pipelines}
|
||||
alias Memex.Pipelines.{Pipeline, Steps, Steps.Step}
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
@ -9,13 +10,128 @@ defmodule MemexWeb.PipelineLive.Show do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:pipeline, Pipelines.get_pipeline!(id))}
|
||||
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}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "show pipeline"
|
||||
defp page_title(:edit), do: "edit pipeline"
|
||||
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
|
||||
end
|
||||
|
@ -1,43 +1,174 @@
|
||||
<h1>show pipeline</h1>
|
||||
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
|
||||
<h1 class="text-xl">
|
||||
<%= @pipeline.slug %>
|
||||
</h1>
|
||||
|
||||
<%= if @live_action in [:edit] do %>
|
||||
<.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>
|
||||
<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>
|
||||
<% _ -> %>
|
||||
<% 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>
|
||||
|
74
lib/memex_web/live/step_live/form_component.ex
Normal file
74
lib/memex_web/live/step_live/form_component.ex
Normal file
@ -0,0 +1,74 @@
|
||||
defmodule MemexWeb.StepLive.FormComponent do
|
||||
use MemexWeb, :live_component
|
||||
|
||||
alias Memex.Pipelines.Steps
|
||||
|
||||
@impl true
|
||||
def update(%{step: step, current_user: current_user, pipeline: _pipeline} = assigns, socket) do
|
||||
changeset = Steps.change_step(step, current_user)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"validate",
|
||||
%{"step" => step_params},
|
||||
%{assigns: %{step: step, current_user: current_user}} = socket
|
||||
) do
|
||||
changeset =
|
||||
step
|
||||
|> Steps.change_step(step_params, current_user)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"step" => step_params}, %{assigns: %{action: action}} = socket) do
|
||||
save_step(socket, action, step_params)
|
||||
end
|
||||
|
||||
defp save_step(
|
||||
%{assigns: %{step: step, return_to: return_to, current_user: current_user}} = socket,
|
||||
:edit_step,
|
||||
step_params
|
||||
) do
|
||||
case Steps.update_step(step, step_params, current_user) do
|
||||
{:ok, %{title: title}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, gettext("%{title} saved", title: title))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_step(
|
||||
%{
|
||||
assigns: %{
|
||||
step: %{position: position},
|
||||
return_to: return_to,
|
||||
current_user: current_user,
|
||||
pipeline: pipeline
|
||||
}
|
||||
} = socket,
|
||||
:add_step,
|
||||
step_params
|
||||
) do
|
||||
case Steps.create_step(step_params, position, pipeline, current_user) do
|
||||
{:ok, %{title: title}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, gettext("%{title} created", title: title))
|
||||
|> push_navigate(to: return_to)}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
end
|
34
lib/memex_web/live/step_live/form_component.html.heex
Normal file
34
lib/memex_web/live/step_live/form_component.html.heex
Normal file
@ -0,0 +1,34 @@
|
||||
<div class="h-full flex flex-col justify-start items-stretch space-y-4">
|
||||
<.form
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
id="step-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
phx-debounce="300"
|
||||
class="flex flex-col justify-start items-stretch space-y-4"
|
||||
>
|
||||
<%= text_input(f, :title,
|
||||
class: "input input-primary",
|
||||
placeholder: gettext("title")
|
||||
) %>
|
||||
<%= error_tag(f, :title) %>
|
||||
|
||||
<%= textarea(f, :content,
|
||||
id: "step-form-content",
|
||||
class: "input input-primary h-64 min-h-64",
|
||||
phx_hook: "MaintainAttrs",
|
||||
phx_update: "ignore",
|
||||
placeholder: gettext("use [[context-slug]] to link to a context")
|
||||
) %>
|
||||
<%= error_tag(f, :content) %>
|
||||
|
||||
<div class="flex justify-center items-stretch space-x-4">
|
||||
<%= submit(dgettext("actions", "save"),
|
||||
phx_disable_with: gettext("saving..."),
|
||||
class: "mx-auto btn btn-primary"
|
||||
) %>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
3
lib/memex_web/not_found_error.ex
Normal file
3
lib/memex_web/not_found_error.ex
Normal file
@ -0,0 +1,3 @@
|
||||
defmodule MemexWeb.NotFoundError do
|
||||
defexception [:message, plug_status: 404]
|
||||
end
|
@ -36,6 +36,7 @@ defmodule MemexWeb.Router do
|
||||
pipe_through :browser
|
||||
|
||||
live "/", HomeLive
|
||||
live "/faq", FaqLive
|
||||
end
|
||||
|
||||
## Authentication routes
|
||||
@ -58,16 +59,18 @@ defmodule MemexWeb.Router do
|
||||
pipe_through [:browser, :require_authenticated_user]
|
||||
|
||||
live "/notes/new", NoteLive.Index, :new
|
||||
live "/notes/:id/edit", NoteLive.Index, :edit
|
||||
live "/note/:id/edit", NoteLive.Show, :edit
|
||||
live "/notes/:slug/edit", NoteLive.Index, :edit
|
||||
live "/note/:slug/edit", NoteLive.Show, :edit
|
||||
|
||||
live "/contexts/new", ContextLive.Index, :new
|
||||
live "/contexts/:id/edit", ContextLive.Index, :edit
|
||||
live "/contexts/:id/show/edit", ContextLive.Show, :edit
|
||||
live "/contexts/:slug/edit", ContextLive.Index, :edit
|
||||
live "/context/:slug/edit", ContextLive.Show, :edit
|
||||
|
||||
live "/pipelines/new", PipelineLive.Index, :new
|
||||
live "/pipelines/:id/edit", PipelineLive.Index, :edit
|
||||
live "/pipelines/:id/show/edit", PipelineLive.Show, :edit
|
||||
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
|
||||
|
||||
get "/users/settings", UserSettingsController, :edit
|
||||
put "/users/settings", UserSettingsController, :update
|
||||
@ -80,13 +83,15 @@ defmodule MemexWeb.Router do
|
||||
|
||||
live "/notes", NoteLive.Index, :index
|
||||
live "/notes/:search", NoteLive.Index, :search
|
||||
live "/note/:id", NoteLive.Show, :show
|
||||
live "/note/:slug", NoteLive.Show, :show
|
||||
|
||||
live "/contexts", ContextLive.Index, :index
|
||||
live "/contexts/:id", ContextLive.Show, :show
|
||||
live "/contexts/:search", ContextLive.Index, :search
|
||||
live "/context/:slug", ContextLive.Show, :show
|
||||
|
||||
live "/pipelines", PipelineLive.Index, :index
|
||||
live "/pipelines/:id", PipelineLive.Show, :show
|
||||
live "/pipelines/:search", PipelineLive.Index, :search
|
||||
live "/pipeline/:slug", PipelineLive.Show, :show
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<script defer type="text/javascript" src="/js/app.js">
|
||||
</script>
|
||||
</head>
|
||||
<body class="pb-8 m-0 p-0 w-full h-full">
|
||||
<body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased">
|
||||
<header>
|
||||
<.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>
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
6
mix.exs
6
mix.exs
@ -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",
|
||||
|
@ -10,95 +10,148 @@
|
||||
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
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:11
|
||||
msgid "Invite someone new!"
|
||||
msgid "Resend confirmation instructions"
|
||||
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
|
||||
#: lib/memex_web/components/topbar.ex:119
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/invite_live/form_component.html.heex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_reset_password/new.html.heex:15
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Send instructions to reset password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:15
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:44
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "change email"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:113
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:131
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "change language"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:58
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:99
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "change password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create invite"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/context_live/index.html.heex:49
|
||||
#: lib/memex_web/live/context_live/show.html.heex:34
|
||||
#: lib/memex_web/live/note_live/index.html.heex:49
|
||||
#: lib/memex_web/live/note_live/show.html.heex:38
|
||||
#: lib/memex_web/live/pipeline_live/index.html.heex:49
|
||||
#: lib/memex_web/live/pipeline_live/show.html.heex:43
|
||||
#: lib/memex_web/live/pipeline_live/show.html.heex:113
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "delete"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:145
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "delete user"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/context_live/index.html.heex:38
|
||||
#: lib/memex_web/live/context_live/show.html.heex:23
|
||||
#: lib/memex_web/live/note_live/index.html.heex:38
|
||||
#: lib/memex_web/live/note_live/show.html.heex:27
|
||||
#: lib/memex_web/live/pipeline_live/index.html.heex:38
|
||||
#: lib/memex_web/live/pipeline_live/show.html.heex:32
|
||||
#: lib/memex_web/live/pipeline_live/show.html.heex:102
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "edit"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "invite someone new!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/components/topbar.ex:125
|
||||
#: lib/memex_web/templates/user_confirmation/new.html.heex:29
|
||||
#: lib/memex_web/templates/user_registration/new.html.heex:47
|
||||
#: lib/memex_web/templates/user_registration/new.html.heex:48
|
||||
#: lib/memex_web/templates/user_reset_password/edit.html.heex:47
|
||||
#: lib/memex_web/templates/user_reset_password/new.html.heex:29
|
||||
#: lib/memex_web/templates/user_session/new.html.heex:3
|
||||
#: lib/memex_web/templates/user_session/new.html.heex:32
|
||||
msgid "Log in"
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "log in"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/context_live/index.html.heex:58
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:111
|
||||
#: lib/memex_web/templates/user_confirmation/new.html.heex:24
|
||||
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/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:42
|
||||
#: lib/memex_web/templates/user_reset_password/new.html.heex:24
|
||||
#: lib/memex_web/templates/user_session/new.html.heex:39
|
||||
msgid "Register"
|
||||
#: 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"
|
||||
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
|
||||
#: lib/memex_web/templates/user_confirmation/new.html.heex:3
|
||||
#: lib/memex_web/templates/user_confirmation/new.html.heex:15
|
||||
msgid "Resend confirmation instructions"
|
||||
msgid "save"
|
||||
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
|
||||
#: 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"
|
||||
msgid "back"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/pipeline_live/show.html.heex:129
|
||||
#, elixir-autogen, elixir-format
|
||||
#: 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"
|
||||
msgid "add step"
|
||||
msgstr ""
|
||||
|
@ -10,297 +10,627 @@
|
||||
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 ""
|
||||
|
||||
#, 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
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm your account"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:63
|
||||
msgid "Contexts"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:22
|
||||
msgid "Contexts:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:70
|
||||
msgid "Convenient:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:32
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:87
|
||||
msgid "Current password"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:58
|
||||
msgid "Disable"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:15
|
||||
msgid "Document notes about individual items or concepts"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:35
|
||||
msgid "Document your processes, attaching contexts to each step"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:33
|
||||
msgid "Edit Invite"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:62
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_registration/new.html.heex:36
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:126
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:44
|
||||
msgid "Features"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_reset_password_controller.ex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Forgot your password?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.ex:12
|
||||
msgid "Home"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/invite_card.ex:25
|
||||
msgid "Invite Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:78
|
||||
#: lib/memex_web/live/invite_live/index.ex:41
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:3
|
||||
msgid "Invites"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_session/new.html.heex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Keep me logged in for 60 days"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_registration/new.html.heex:32
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Language"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/layout/live.html.heex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_session_controller.ex:8
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:50
|
||||
msgid "Multi-user:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/form_component.html.heex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:37
|
||||
msgid "New Invite"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:71
|
||||
msgid "New password"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:8
|
||||
msgid "No invites 😔"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:70
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:12
|
||||
msgid "Notes:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:56
|
||||
msgid "Pipelines"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:32
|
||||
msgid "Pipelines:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:63
|
||||
msgid "Privacy controls on a per-note, context or pipeline basis"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:60
|
||||
msgid "Privacy:"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:25
|
||||
msgid "Provide context around a single topic and hotlink to your notes"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/layout/live.html.heex:50
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reconnecting..."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_registration_controller.ex:35
|
||||
msgid "Register"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_reset_password_controller.ex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reset your password"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:78
|
||||
msgid "Set Unlimited"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:10
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/form_component.html.heex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
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
|
||||
#: lib/memex_web/live/home_live.html.heex:87
|
||||
msgid "Admins:"
|
||||
msgid "org-mode"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/note_live/form_component.html.heex:20
|
||||
msgid "Content"
|
||||
msgid "some things that this memex is very loosely inspired by:"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:88
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:134
|
||||
msgid "Get involved!"
|
||||
msgid "spoons? probably not. a particular brand of spoons that you really like? why not :)"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:151
|
||||
msgid "Help translate"
|
||||
msgid "this is a memex, used to document not just your notes, but also your perspectives and processes."
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:96
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:82
|
||||
msgid "Instance Information"
|
||||
msgid "what should my contexts be like?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:80
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:113
|
||||
msgid "Invite Only"
|
||||
msgid "what should my notes be like?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:110
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:112
|
||||
msgid "Public Signups"
|
||||
msgid "what should my pipelines be like?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:160
|
||||
msgid "Report bugs or request features"
|
||||
msgid "why split up into notes, contexts and pipelines?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/live/faq_live.html.heex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
#: 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"
|
||||
msgid "zettelkasten"
|
||||
msgstr ""
|
||||
|
@ -10,83 +10,83 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex/accounts/email.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/email/confirm_email.txt.eex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "If you didn't create an account at %{url}, please ignore this."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/email/confirm_email.html.heex:22
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "If you didn't create an account at Memex, please ignore this."
|
||||
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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex/accounts/email.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Reset your Memex password"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/layout/email.txt.eex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This email was sent from Memex at %{url}, the self-hosted firearm tracker website."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/layout/email.html.heex:13
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This email was sent from Memex, the self-hosted firearm tracker website."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex/accounts/email.ex:44
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
@ -10,109 +10,123 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:84
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Email change link is invalid or it has expired."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/error/error.html.heex:8
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/error/error.html.heex:28
|
||||
msgid "Go back home"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/views/error_view.ex:11
|
||||
msgid "Internal Server Error"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_session_controller.ex:17
|
||||
#, elixir-autogen, elixir-format
|
||||
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
|
||||
#: 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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_reset_password_controller.ex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:99
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unable to delete user"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/views/error_view.ex:10
|
||||
msgid "Unauthorized"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_confirmation_controller.ex:54
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User confirmation link is invalid or it has expired."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:18
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You are not authorized to view this page"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_auth.ex:177
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
@ -10,138 +10,149 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_confirmation_controller.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{email} confirmed successfully."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/form_component.ex:62
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} created successfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} deleted succesfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:114
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} disabled succesfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:90
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} enabled succesfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:68
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} updated succesfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/form_component.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{invite_name} updated successfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:139
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "%{user_email} deleted succesfully"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "A link to confirm your email change has been sent to the new address."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:133
|
||||
msgid "Are you sure you want to change your language?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:101
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:130
|
||||
msgid "Are you sure you want to delete %{email}? This action is permanent!"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:48
|
||||
msgid "Are you sure you want to delete the invite for %{invite_name}?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:143
|
||||
msgid "Are you sure you want to delete your account?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/components/topbar.ex:95
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.html.heex:73
|
||||
msgid "Are you sure you want to make %{invite_name} unlimited?"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/index.ex:127
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Copied to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:77
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Email changed successfully."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_confirmation_controller.ex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_reset_password_controller.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "If your email is in our system, you will receive instructions to reset your password shortly."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:65
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Language updated successfully."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_session_controller.ex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Logged out successfully."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_reset_password_controller.ex:46
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password reset successfully."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:49
|
||||
#, elixir-autogen, elixir-format
|
||||
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 ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/invite_live/form_component.html.heex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Saving..."
|
||||
msgstr ""
|
||||
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/controllers/user_settings_controller.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Your account has been deleted"
|
||||
msgstr ""
|
||||
|
||||
#: lib/memex_web/templates/user_settings/edit.html.heex:133
|
||||
#, elixir-autogen, elixir-format
|
||||
#: lib/memex_web/live/home_live.html.heex:91
|
||||
msgid "Register to setup %{name}"
|
||||
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}"
|
||||
msgstr ""
|
||||
|
@ -6,10 +6,30 @@ defmodule Memex.Repo.Migrations.CreateContexts do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :title, :string
|
||||
add :content, :text
|
||||
add :tag, {:array, :string}
|
||||
add :tags, {: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
|
||||
|
@ -6,9 +6,30 @@ 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
|
||||
|
@ -5,13 +5,16 @@ defmodule Memex.Repo.Migrations.CreateSteps do
|
||||
create table(:steps, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :title, :string
|
||||
add :description, :text
|
||||
add :content, :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
|
||||
|
@ -1,16 +0,0 @@
|
||||
defmodule Memex.Repo.Migrations.CreateStepContexts do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:step_contexts, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :step_id, references(:steps, on_delete: :nothing, type: :binary_id)
|
||||
add :context_id, references(:contexts, on_delete: :nothing, type: :binary_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:step_contexts, [:step_id])
|
||||
create index(:step_contexts, [:context_id])
|
||||
end
|
||||
end
|
@ -1,16 +0,0 @@
|
||||
defmodule Memex.Repo.Migrations.CreateContextNotes do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:context_notes, primary_key: false) do
|
||||
add :id, :binary_id, primary_key: true
|
||||
add :context_id, references(:contexts, on_delete: :nothing, type: :binary_id)
|
||||
add :note_id, references(:notes, on_delete: :nothing, type: :binary_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create index(:context_notes, [:context_id])
|
||||
create index(:context_notes, [:note_id])
|
||||
end
|
||||
end
|
14
priv/repo/migrations/20221126182210_use_slugs.exs
Normal file
14
priv/repo/migrations/20221126182210_use_slugs.exs
Normal file
@ -0,0 +1,14 @@
|
||||
defmodule Memex.Repo.Migrations.UseSlugs do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
rename table(:notes), :title, to: :slug
|
||||
create unique_index(:notes, [:slug])
|
||||
|
||||
rename table(:contexts), :title, to: :slug
|
||||
create unique_index(:contexts, [:slug])
|
||||
|
||||
rename table(:pipelines), :title, to: :slug
|
||||
create unique_index(:pipelines, [:slug])
|
||||
end
|
||||
end
|
16
readme.md
16
readme.md
@ -1,6 +1,8 @@
|
||||
# Memex
|
||||
# memEx
|
||||
|
||||
memex is an easy way to digitize the structured processes of your life.
|
||||
![old screenshot](https://gitea.bubbletea.dev/shibao/memEx/raw/branch/stable/home.png)
|
||||
|
||||
memEx is an easy way to digitize the structured processes of your life.
|
||||
|
||||
- Notes: Document notes about individual items or concepts
|
||||
- Contexts: Provide context around a single topic and hotlink to individual
|
||||
@ -16,7 +18,7 @@ memex is an easy way to digitize the structured processes of your life.
|
||||
# Installation
|
||||
|
||||
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
|
||||
@ -27,8 +29,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`
|
||||
@ -55,4 +57,6 @@ 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)
|
||||
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/)
|
||||
|
@ -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(%User{})
|
||||
assert %Changeset{} = changeset = Accounts.change_user_registration()
|
||||
assert changeset.required == [:password, :email]
|
||||
end
|
||||
|
||||
@ -112,8 +112,7 @@ defmodule Memex.AccountsTest do
|
||||
email = unique_user_email()
|
||||
password = valid_user_password()
|
||||
|
||||
changeset =
|
||||
Accounts.change_user_registration(%User{}, %{"email" => email, "password" => password})
|
||||
changeset = Accounts.change_user_registration(%{"email" => email, "password" => password})
|
||||
|
||||
assert changeset.valid?
|
||||
assert get_change(changeset, :email) == email
|
||||
|
@ -1,71 +1,138 @@
|
||||
defmodule Memex.ContextsTest do
|
||||
use Memex.DataCase
|
||||
|
||||
alias Memex.Contexts
|
||||
import Memex.ContextsFixtures
|
||||
alias Memex.{Contexts, Contexts.Context}
|
||||
@moduletag :contexts_test
|
||||
@invalid_attrs %{content: nil, tag: nil, slug: nil, visibility: nil}
|
||||
|
||||
describe "contexts" do
|
||||
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]
|
||||
setup do
|
||||
[user: user_fixture()]
|
||||
end
|
||||
|
||||
test "get_context!/1 returns the context with given id" do
|
||||
context = context_fixture()
|
||||
assert Contexts.get_context!(context.id) == context
|
||||
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]
|
||||
end
|
||||
|
||||
test "create_context/1 with valid data creates a context" do
|
||||
valid_attrs = %{content: "some content", tag: [], title: "some title", visibility: :public}
|
||||
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
|
||||
|
||||
assert {:ok, %Context{} = context} = Contexts.create_context(valid_attrs)
|
||||
test "get_context!/1 returns the context with given id", %{user: user} do
|
||||
context = context_fixture(%{visibility: :public}, user)
|
||||
assert Contexts.get_context!(context.id, user) == context
|
||||
|
||||
context = context_fixture(%{visibility: :unlisted}, user)
|
||||
assert Contexts.get_context!(context.id, user) == context
|
||||
|
||||
context = context_fixture(%{visibility: :private}, user)
|
||||
assert Contexts.get_context!(context.id, user) == context
|
||||
end
|
||||
|
||||
test "get_context!/1 only returns unlisted or public contexts for other users", %{user: user} do
|
||||
another_user = user_fixture()
|
||||
context = context_fixture(%{visibility: :public}, another_user)
|
||||
assert Contexts.get_context!(context.id, user) == context
|
||||
|
||||
context = context_fixture(%{visibility: :unlisted}, another_user)
|
||||
assert Contexts.get_context!(context.id, user) == context
|
||||
|
||||
context = context_fixture(%{visibility: :private}, another_user)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Contexts.get_context!(context.id, user)
|
||||
end
|
||||
end
|
||||
|
||||
test "get_context_by_slug/1 returns the context with given id", %{user: user} do
|
||||
context = context_fixture(%{slug: "a", visibility: :public}, user)
|
||||
assert Contexts.get_context_by_slug("a", user) == context
|
||||
|
||||
context = context_fixture(%{slug: "b", visibility: :unlisted}, user)
|
||||
assert Contexts.get_context_by_slug("b", user) == context
|
||||
|
||||
context = context_fixture(%{slug: "c", visibility: :private}, user)
|
||||
assert Contexts.get_context_by_slug("c", user) == context
|
||||
end
|
||||
|
||||
test "get_context_by_slug/1 only returns unlisted or public contexts for other users", %{
|
||||
user: user
|
||||
} do
|
||||
another_user = user_fixture()
|
||||
context = context_fixture(%{slug: "a", visibility: :public}, another_user)
|
||||
assert Contexts.get_context_by_slug("a", user) == context
|
||||
|
||||
context = context_fixture(%{slug: "b", visibility: :unlisted}, another_user)
|
||||
assert Contexts.get_context_by_slug("b", user) == context
|
||||
|
||||
context_fixture(%{slug: "c", visibility: :private}, another_user)
|
||||
assert Contexts.get_context_by_slug("c", user) |> is_nil()
|
||||
end
|
||||
|
||||
test "create_context/1 with valid data creates a context", %{user: user} do
|
||||
valid_attrs = %{
|
||||
"content" => "some content",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
|
||||
assert {:ok, %Context{} = context} = Contexts.create_context(valid_attrs, user)
|
||||
assert context.content == "some content"
|
||||
assert context.tag == []
|
||||
assert context.title == "some title"
|
||||
assert context.tags == ["tag1", "tag2"]
|
||||
assert context.slug == "some-slug"
|
||||
assert context.visibility == :public
|
||||
end
|
||||
|
||||
test "create_context/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Contexts.create_context(@invalid_attrs)
|
||||
test "create_context/1 with invalid data returns error changeset", %{user: user} do
|
||||
assert {:error, %Ecto.Changeset{}} = Contexts.create_context(@invalid_attrs, user)
|
||||
end
|
||||
|
||||
test "update_context/2 with valid data updates the context" do
|
||||
context = context_fixture()
|
||||
test "update_context/2 with valid data updates the context", %{user: user} do
|
||||
context = context_fixture(user)
|
||||
|
||||
update_attrs = %{
|
||||
content: "some updated content",
|
||||
tag: [],
|
||||
title: "some updated title",
|
||||
visibility: :private
|
||||
"content" => "some updated content",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"slug" => "some-updated-slug",
|
||||
"visibility" => :private
|
||||
}
|
||||
|
||||
assert {:ok, %Context{} = context} = Contexts.update_context(context, update_attrs)
|
||||
assert {:ok, %Context{} = context} = Contexts.update_context(context, update_attrs, user)
|
||||
assert context.content == "some updated content"
|
||||
assert context.tag == []
|
||||
assert context.title == "some updated title"
|
||||
assert context.tags == ["tag1", "tag2"]
|
||||
assert context.slug == "some-updated-slug"
|
||||
assert context.visibility == :private
|
||||
end
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
test "change_context/1 returns a context changeset" do
|
||||
context = context_fixture()
|
||||
assert %Ecto.Changeset{} = Contexts.change_context(context)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,8 +2,8 @@ defmodule Memex.NotesTest do
|
||||
use Memex.DataCase
|
||||
import Memex.NotesFixtures
|
||||
alias Memex.{Notes, Notes.Note}
|
||||
|
||||
@invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil}
|
||||
@moduletag :notes_test
|
||||
@invalid_attrs %{content: nil, tag: nil, slug: 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(%{title: "a", visibility: :public}, user)
|
||||
note_b = note_fixture(%{title: "b", visibility: :unlisted}, user)
|
||||
note_c = note_fixture(%{title: "c", visibility: :private}, user)
|
||||
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)
|
||||
assert Notes.list_notes(user) == [note_a, note_b, note_c]
|
||||
end
|
||||
|
||||
@ -25,22 +25,68 @@ defmodule Memex.NotesTest do
|
||||
end
|
||||
|
||||
test "get_note!/1 returns the note with given id", %{user: user} do
|
||||
note = note_fixture(user)
|
||||
note = note_fixture(%{visibility: :public}, user)
|
||||
assert Notes.get_note!(note.id, user) == note
|
||||
|
||||
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",
|
||||
"title" => "some title",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
|
||||
assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user)
|
||||
assert note.content == "some content"
|
||||
assert note.tags == ["tag1", "tag2"]
|
||||
assert note.title == "some title"
|
||||
assert note.slug == "some-slug"
|
||||
assert note.visibility == :public
|
||||
end
|
||||
|
||||
@ -54,14 +100,14 @@ defmodule Memex.NotesTest do
|
||||
update_attrs = %{
|
||||
"content" => "some updated content",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"title" => "some updated title",
|
||||
"slug" => "some-updated-slug",
|
||||
"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.title == "some updated title"
|
||||
assert note.slug == "some-updated-slug"
|
||||
assert note.visibility == :private
|
||||
end
|
||||
|
||||
@ -77,6 +123,13 @@ 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)
|
||||
|
@ -1,68 +1,145 @@
|
||||
defmodule Memex.PipelinesTest do
|
||||
use Memex.DataCase
|
||||
|
||||
alias Memex.Pipelines
|
||||
import Memex.PipelinesFixtures
|
||||
alias Memex.{Pipelines, Pipelines.Pipeline}
|
||||
@moduletag :pipelines_test
|
||||
@invalid_attrs %{description: nil, tag: nil, slug: nil, visibility: nil}
|
||||
|
||||
describe "pipelines" do
|
||||
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]
|
||||
setup do
|
||||
[user: user_fixture()]
|
||||
end
|
||||
|
||||
test "get_pipeline!/1 returns the pipeline with given id" do
|
||||
pipeline = pipeline_fixture()
|
||||
assert Pipelines.get_pipeline!(pipeline.id) == pipeline
|
||||
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]
|
||||
end
|
||||
|
||||
test "create_pipeline/1 with valid data creates a pipeline" do
|
||||
valid_attrs = %{description: "some description", title: "some title", visibility: :public}
|
||||
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
|
||||
|
||||
assert {:ok, %Pipeline{} = pipeline} = Pipelines.create_pipeline(valid_attrs)
|
||||
test "get_pipeline!/1 returns the pipeline with given id", %{user: user} do
|
||||
pipeline = pipeline_fixture(%{visibility: :public}, user)
|
||||
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{visibility: :unlisted}, user)
|
||||
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{visibility: :private}, user)
|
||||
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
|
||||
end
|
||||
|
||||
test "get_pipeline!/1 only returns unlisted or public pipelines for other users", %{
|
||||
user: user
|
||||
} do
|
||||
another_user = user_fixture()
|
||||
pipeline = pipeline_fixture(%{visibility: :public}, another_user)
|
||||
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{visibility: :unlisted}, another_user)
|
||||
assert Pipelines.get_pipeline!(pipeline.id, user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{visibility: :private}, another_user)
|
||||
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
Pipelines.get_pipeline!(pipeline.id, user)
|
||||
end
|
||||
end
|
||||
|
||||
test "get_pipeline_by_slug/1 returns the pipeline with given id", %{user: user} do
|
||||
pipeline = pipeline_fixture(%{slug: "a", visibility: :public}, user)
|
||||
assert Pipelines.get_pipeline_by_slug("a", user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{slug: "b", visibility: :unlisted}, user)
|
||||
assert Pipelines.get_pipeline_by_slug("b", user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{slug: "c", visibility: :private}, user)
|
||||
assert Pipelines.get_pipeline_by_slug("c", user) == pipeline
|
||||
end
|
||||
|
||||
test "get_pipeline_by_slug/1 only returns unlisted or public pipelines for other users", %{
|
||||
user: user
|
||||
} do
|
||||
another_user = user_fixture()
|
||||
pipeline = pipeline_fixture(%{slug: "a", visibility: :public}, another_user)
|
||||
assert Pipelines.get_pipeline_by_slug("a", user) == pipeline
|
||||
|
||||
pipeline = pipeline_fixture(%{slug: "b", visibility: :unlisted}, another_user)
|
||||
assert Pipelines.get_pipeline_by_slug("b", user) == pipeline
|
||||
|
||||
pipeline_fixture(%{slug: "c", visibility: :private}, another_user)
|
||||
assert Pipelines.get_pipeline_by_slug("c", user) |> is_nil()
|
||||
end
|
||||
|
||||
test "create_pipeline/1 with valid data creates a pipeline", %{user: user} do
|
||||
valid_attrs = %{
|
||||
"description" => "some description",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
|
||||
assert {:ok, %Pipeline{} = pipeline} = Pipelines.create_pipeline(valid_attrs, user)
|
||||
assert pipeline.description == "some description"
|
||||
assert pipeline.title == "some title"
|
||||
assert pipeline.tags == ["tag1", "tag2"]
|
||||
assert pipeline.slug == "some-slug"
|
||||
assert pipeline.visibility == :public
|
||||
end
|
||||
|
||||
test "create_pipeline/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Pipelines.create_pipeline(@invalid_attrs)
|
||||
test "create_pipeline/1 with invalid data returns error changeset", %{user: user} do
|
||||
assert {:error, %Ecto.Changeset{}} = Pipelines.create_pipeline(@invalid_attrs, user)
|
||||
end
|
||||
|
||||
test "update_pipeline/2 with valid data updates the pipeline" do
|
||||
pipeline = pipeline_fixture()
|
||||
test "update_pipeline/2 with valid data updates the pipeline", %{user: user} do
|
||||
pipeline = pipeline_fixture(user)
|
||||
|
||||
update_attrs = %{
|
||||
description: "some updated description",
|
||||
title: "some updated title",
|
||||
visibility: :private
|
||||
"description" => "some updated description",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"slug" => "some-updated-slug",
|
||||
"visibility" => :private
|
||||
}
|
||||
|
||||
assert {:ok, %Pipeline{} = pipeline} = Pipelines.update_pipeline(pipeline, update_attrs)
|
||||
assert {:ok, %Pipeline{} = pipeline} =
|
||||
Pipelines.update_pipeline(pipeline, update_attrs, user)
|
||||
|
||||
assert pipeline.description == "some updated description"
|
||||
assert pipeline.title == "some updated title"
|
||||
assert pipeline.tags == ["tag1", "tag2"]
|
||||
assert pipeline.slug == "some-updated-slug"
|
||||
assert pipeline.visibility == :private
|
||||
end
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
test "change_pipeline/1 returns a pipeline changeset" do
|
||||
pipeline = pipeline_fixture()
|
||||
assert %Ecto.Changeset{} = Pipelines.change_pipeline(pipeline)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,68 +1,148 @@
|
||||
defmodule Memex.StepsTest do
|
||||
use Memex.DataCase
|
||||
|
||||
alias Memex.Steps
|
||||
import Memex.{PipelinesFixtures, StepsFixtures}
|
||||
alias Memex.Pipelines.{Steps, Steps.Step}
|
||||
@moduletag :steps_test
|
||||
@invalid_attrs %{content: nil, title: nil}
|
||||
|
||||
describe "steps" do
|
||||
alias Memex.Steps.Step
|
||||
setup do
|
||||
user = user_fixture()
|
||||
pipeline = pipeline_fixture(user)
|
||||
|
||||
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]
|
||||
[user: user, pipeline: pipeline]
|
||||
end
|
||||
|
||||
test "get_step!/1 returns the step with given id" do
|
||||
step = step_fixture()
|
||||
assert Steps.get_step!(step.id) == step
|
||||
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]
|
||||
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 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
|
||||
|
||||
assert {:ok, %Step{} = step} = Steps.create_step(valid_attrs)
|
||||
assert step.description == "some description"
|
||||
assert step.position == 42
|
||||
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 step.title == "some title"
|
||||
end
|
||||
|
||||
test "create_step/1 with invalid data returns error changeset" do
|
||||
assert {:error, %Ecto.Changeset{}} = Steps.create_step(@invalid_attrs)
|
||||
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)
|
||||
end
|
||||
|
||||
test "update_step/2 with valid data updates the step" do
|
||||
step = step_fixture()
|
||||
test "update_step/3 with valid data updates the step", %{pipeline: pipeline, user: user} do
|
||||
step = step_fixture(0, pipeline, user)
|
||||
|
||||
update_attrs = %{
|
||||
description: "some updated description",
|
||||
position: 43,
|
||||
title: "some updated title"
|
||||
"content" => "some updated content",
|
||||
"title" => "some updated title"
|
||||
}
|
||||
|
||||
assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs)
|
||||
assert step.description == "some updated description"
|
||||
assert step.position == 43
|
||||
assert {:ok, %Step{} = step} = Steps.update_step(step, update_attrs, user)
|
||||
assert step.content == "some updated content"
|
||||
assert step.title == "some updated title"
|
||||
end
|
||||
|
||||
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)
|
||||
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)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
test "change_step/1 returns a step changeset" do
|
||||
step = step_fixture()
|
||||
assert %Ecto.Changeset{} = Steps.change_step(step)
|
||||
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)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -1,40 +1,39 @@
|
||||
defmodule MemexWeb.ContextLiveTest do
|
||||
use MemexWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Memex.ContextsFixtures
|
||||
import Memex.{ContextsFixtures, NotesFixtures}
|
||||
alias MemexWeb.Endpoint
|
||||
|
||||
@create_attrs %{
|
||||
"content" => "some content",
|
||||
"tags_string" => "tag1",
|
||||
"title" => "some title",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
@update_attrs %{
|
||||
"content" => "some updated content",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"title" => "some updated title",
|
||||
"slug" => "some-updated-slug",
|
||||
"visibility" => :private
|
||||
}
|
||||
@invalid_attrs %{
|
||||
"content" => nil,
|
||||
"tags_string" => "",
|
||||
"title" => nil,
|
||||
"slug" => nil,
|
||||
"visibility" => nil
|
||||
}
|
||||
|
||||
defp create_context(_) do
|
||||
context = context_fixture()
|
||||
%{context: context}
|
||||
defp create_context(%{user: user}) do
|
||||
[context: context_fixture(user)]
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
setup [:create_context]
|
||||
setup [:register_and_log_in_user, :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 =~ "listing contexts"
|
||||
assert html =~ "contexts"
|
||||
assert html =~ context.content
|
||||
end
|
||||
|
||||
@ -56,17 +55,17 @@ defmodule MemexWeb.ContextLiveTest do
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.context_index_path(conn, :index))
|
||||
|
||||
assert html =~ "context created successfully"
|
||||
assert html =~ "#{@create_attrs |> Map.get("slug")} created"
|
||||
assert html =~ "some content"
|
||||
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("#context-#{context.id} a", "edit") |> render_click() =~
|
||||
"edit context"
|
||||
assert index_live |> element("[data-qa=\"context-edit-#{context.id}\"]") |> render_click() =~
|
||||
"edit"
|
||||
|
||||
assert_patch(index_live, Routes.context_index_path(conn, :edit, context))
|
||||
assert_patch(index_live, Routes.context_index_path(conn, :edit, context.slug))
|
||||
|
||||
assert index_live
|
||||
|> form("#context-form", context: @invalid_attrs)
|
||||
@ -78,35 +77,34 @@ defmodule MemexWeb.ContextLiveTest do
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.context_index_path(conn, :index))
|
||||
|
||||
assert html =~ "context updated successfully"
|
||||
assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
|
||||
assert html =~ "some updated content"
|
||||
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("#context-#{context.id} a", "delete") |> render_click()
|
||||
assert index_live |> element("[data-qa=\"delete-context-#{context.id}\"]") |> render_click()
|
||||
refute has_element?(index_live, "#context-#{context.id}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "show" do
|
||||
setup [:create_context]
|
||||
setup [:register_and_log_in_user, :create_context]
|
||||
|
||||
test "displays context", %{conn: conn, context: context} do
|
||||
{:ok, _show_live, html} = live(conn, Routes.context_show_path(conn, :show, context))
|
||||
{:ok, _show_live, html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
|
||||
|
||||
assert html =~ "show context"
|
||||
assert html =~ "context"
|
||||
assert html =~ context.content
|
||||
end
|
||||
|
||||
test "updates context within modal", %{conn: conn, context: context} do
|
||||
{:ok, show_live, _html} = live(conn, Routes.context_show_path(conn, :show, context))
|
||||
{:ok, show_live, _html} = live(conn, Routes.context_show_path(conn, :show, context.slug))
|
||||
|
||||
assert show_live |> element("a", "edit") |> render_click() =~
|
||||
"edit context"
|
||||
assert show_live |> element("a", "edit") |> render_click() =~ "edit"
|
||||
|
||||
assert_patch(show_live, Routes.context_show_path(conn, :edit, context))
|
||||
assert_patch(show_live, Routes.context_show_path(conn, :edit, context.slug))
|
||||
|
||||
assert show_live
|
||||
|> form("#context-form", context: @invalid_attrs)
|
||||
@ -114,12 +112,46 @@ defmodule MemexWeb.ContextLiveTest do
|
||||
|
||||
{:ok, _, html} =
|
||||
show_live
|
||||
|> form("#context-form", context: @update_attrs)
|
||||
|> form("#context-form", context: Map.put(@update_attrs, "slug", context.slug))
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.context_show_path(conn, :show, context))
|
||||
|> follow_redirect(conn, Routes.context_show_path(conn, :show, context.slug))
|
||||
|
||||
assert html =~ "context updated successfully"
|
||||
assert html =~ "#{context.slug} saved"
|
||||
assert html =~ "some updated content"
|
||||
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
|
||||
|
@ -7,19 +7,19 @@ defmodule MemexWeb.NoteLiveTest do
|
||||
@create_attrs %{
|
||||
"content" => "some content",
|
||||
"tags_string" => "tag1",
|
||||
"title" => "some title",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
@update_attrs %{
|
||||
"content" => "some updated content",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"title" => "some updated title",
|
||||
"slug" => "some-updated-slug",
|
||||
"visibility" => :private
|
||||
}
|
||||
@invalid_attrs %{
|
||||
"content" => nil,
|
||||
"tags_string" => "",
|
||||
"title" => nil,
|
||||
"slug" => 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("title")} created"
|
||||
assert html =~ "#{@create_attrs |> Map.get("slug")} 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))
|
||||
assert_patch(index_live, Routes.note_index_path(conn, :edit, note.slug))
|
||||
|
||||
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("title")} saved"
|
||||
assert html =~ "#{@update_attrs |> Map.get("slug")} 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))
|
||||
{:ok, _show_live, html} = live(conn, Routes.note_show_path(conn, :show, note.slug))
|
||||
|
||||
assert html =~ "show note"
|
||||
assert html =~ "note"
|
||||
assert html =~ note.content
|
||||
end
|
||||
|
||||
test "updates note within modal", %{conn: conn, note: note} do
|
||||
{:ok, show_live, _html} = live(conn, Routes.note_show_path(conn, :show, note))
|
||||
{:ok, show_live, _html} = live(conn, Routes.note_show_path(conn, :show, note.slug))
|
||||
|
||||
assert show_live |> element("a", "edit") |> render_click() =~ "edit"
|
||||
|
||||
assert_patch(show_live, Routes.note_show_path(conn, :edit, note))
|
||||
assert_patch(show_live, Routes.note_show_path(conn, :edit, note.slug))
|
||||
|
||||
assert show_live
|
||||
|> form("#note-form", note: @invalid_attrs)
|
||||
@ -112,12 +112,24 @@ defmodule MemexWeb.NoteLiveTest do
|
||||
|
||||
{:ok, _, html} =
|
||||
show_live
|
||||
|> form("#note-form", note: @update_attrs)
|
||||
|> form("#note-form", note: Map.put(@update_attrs, "slug", note.slug))
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.note_show_path(conn, :show, note))
|
||||
|> follow_redirect(conn, Routes.note_show_path(conn, :show, note.slug))
|
||||
|
||||
assert html =~ "#{@update_attrs |> Map.get("title")} saved"
|
||||
assert html =~ "#{note.slug} saved"
|
||||
assert html =~ "some updated content"
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -1,29 +1,50 @@
|
||||
defmodule MemexWeb.PipelineLiveTest do
|
||||
use MemexWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Memex.PipelinesFixtures
|
||||
import Memex.{PipelinesFixtures, StepsFixtures}
|
||||
|
||||
@create_attrs %{description: "some description", title: "some title", visibility: :public}
|
||||
@update_attrs %{
|
||||
description: "some updated description",
|
||||
title: "some updated title",
|
||||
visibility: :private
|
||||
@create_attrs %{
|
||||
"description" => "some description",
|
||||
"tags_string" => "tag1",
|
||||
"slug" => "some-slug",
|
||||
"visibility" => :public
|
||||
}
|
||||
@update_attrs %{
|
||||
"description" => "some updated description",
|
||||
"tags_string" => "tag1,tag2",
|
||||
"slug" => "some-updated-slug",
|
||||
"visibility" => :private
|
||||
}
|
||||
@invalid_attrs %{
|
||||
"description" => nil,
|
||||
"tags_string" => "",
|
||||
"slug" => nil,
|
||||
"visibility" => nil
|
||||
}
|
||||
@step_create_attrs %{
|
||||
"content" => "some content",
|
||||
"title" => "some title"
|
||||
}
|
||||
@step_update_attrs %{
|
||||
"content" => "some updated content",
|
||||
"title" => "some updated title"
|
||||
}
|
||||
@step_invalid_attrs %{
|
||||
"content" => nil,
|
||||
"title" => nil
|
||||
}
|
||||
@invalid_attrs %{description: nil, title: nil, visibility: nil}
|
||||
|
||||
defp create_pipeline(_) do
|
||||
pipeline = pipeline_fixture()
|
||||
%{pipeline: pipeline}
|
||||
defp create_pipeline(%{user: user}) do
|
||||
[pipeline: pipeline_fixture(user)]
|
||||
end
|
||||
|
||||
describe "Index" do
|
||||
setup [:create_pipeline]
|
||||
setup [:register_and_log_in_user, :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 =~ "listing pipelines"
|
||||
assert html =~ "pipelines"
|
||||
assert html =~ pipeline.description
|
||||
end
|
||||
|
||||
@ -45,17 +66,17 @@ defmodule MemexWeb.PipelineLiveTest do
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
|
||||
|
||||
assert html =~ "pipeline created successfully"
|
||||
assert html =~ "#{@create_attrs |> Map.get("slug")} created"
|
||||
assert html =~ "some description"
|
||||
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("#pipeline-#{pipeline.id} a", "edit") |> render_click() =~
|
||||
"edit pipeline"
|
||||
assert index_live |> element("[data-qa=\"pipeline-edit-#{pipeline.id}\"]") |> render_click() =~
|
||||
"edit"
|
||||
|
||||
assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline))
|
||||
assert_patch(index_live, Routes.pipeline_index_path(conn, :edit, pipeline.slug))
|
||||
|
||||
assert index_live
|
||||
|> form("#pipeline-form", pipeline: @invalid_attrs)
|
||||
@ -67,35 +88,37 @@ defmodule MemexWeb.PipelineLiveTest do
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.pipeline_index_path(conn, :index))
|
||||
|
||||
assert html =~ "pipeline updated successfully"
|
||||
assert html =~ "#{@update_attrs |> Map.get("slug")} saved"
|
||||
assert html =~ "some updated description"
|
||||
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("#pipeline-#{pipeline.id} a", "delete") |> render_click()
|
||||
assert index_live
|
||||
|> element("[data-qa=\"delete-pipeline-#{pipeline.id}\"]")
|
||||
|> render_click()
|
||||
|
||||
refute has_element?(index_live, "#pipeline-#{pipeline.id}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "show" do
|
||||
setup [:create_pipeline]
|
||||
setup [:register_and_log_in_user, :create_pipeline]
|
||||
|
||||
test "displays pipeline", %{conn: conn, pipeline: pipeline} do
|
||||
{:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline))
|
||||
{:ok, _show_live, html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
|
||||
|
||||
assert html =~ "show pipeline"
|
||||
assert html =~ "pipeline"
|
||||
assert html =~ pipeline.description
|
||||
end
|
||||
|
||||
test "updates pipeline within modal", %{conn: conn, pipeline: pipeline} do
|
||||
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline))
|
||||
{:ok, show_live, _html} = live(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
|
||||
|
||||
assert show_live |> element("a", "edit") |> render_click() =~
|
||||
"edit pipeline"
|
||||
assert show_live |> element("a", "edit") |> render_click() =~ "edit"
|
||||
|
||||
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline))
|
||||
assert_patch(show_live, Routes.pipeline_show_path(conn, :edit, pipeline.slug))
|
||||
|
||||
assert show_live
|
||||
|> form("#pipeline-form", pipeline: @invalid_attrs)
|
||||
@ -103,12 +126,129 @@ defmodule MemexWeb.PipelineLiveTest do
|
||||
|
||||
{:ok, _, html} =
|
||||
show_live
|
||||
|> form("#pipeline-form", pipeline: @update_attrs)
|
||||
|> form("#pipeline-form", pipeline: Map.put(@update_attrs, "slug", pipeline.slug))
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline))
|
||||
|> follow_redirect(conn, Routes.pipeline_show_path(conn, :show, pipeline.slug))
|
||||
|
||||
assert html =~ "pipeline updated successfully"
|
||||
assert html =~ "#{pipeline.slug} saved"
|
||||
assert html =~ "some updated description"
|
||||
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'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
|
||||
|
@ -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
|
||||
|
@ -2,8 +2,7 @@ defmodule Memex.Fixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating entities
|
||||
"""
|
||||
|
||||
alias Memex.{Accounts, Accounts.User, Email}
|
||||
alias Memex.{Accounts, Accounts.User, Email, Repo}
|
||||
|
||||
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||
def valid_user_password, do: "hello world!"
|
||||
@ -26,11 +25,12 @@ defmodule Memex.Fixtures do
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
"email" => unique_user_email(),
|
||||
"password" => valid_user_password(),
|
||||
"role" => "admin"
|
||||
"password" => valid_user_password()
|
||||
})
|
||||
|> Accounts.register_user()
|
||||
|> unwrap_ok_tuple()
|
||||
|> User.role_changeset("admin")
|
||||
|> Repo.update!()
|
||||
end
|
||||
|
||||
def extract_user_token(fun) do
|
||||
@ -56,5 +56,14 @@ 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
|
||||
|
@ -3,20 +3,24 @@ 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.
|
||||
"""
|
||||
def context_fixture(attrs \\ %{}) do
|
||||
@spec context_fixture(User.t()) :: Context.t()
|
||||
@spec context_fixture(attrs :: map(), User.t()) :: Context.t()
|
||||
def context_fixture(attrs \\ %{}, user) do
|
||||
{:ok, context} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
content: "some content",
|
||||
tag: [],
|
||||
title: "some title",
|
||||
visibility: :public
|
||||
slug: random_slug(),
|
||||
visibility: :private
|
||||
})
|
||||
|> Memex.Contexts.create_context()
|
||||
|> Contexts.create_context(user)
|
||||
|
||||
context
|
||||
end
|
||||
|
@ -3,6 +3,7 @@ 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 """
|
||||
@ -16,7 +17,7 @@ defmodule Memex.NotesFixtures do
|
||||
|> Enum.into(%{
|
||||
content: "some content",
|
||||
tag: [],
|
||||
title: "some title",
|
||||
slug: random_slug(),
|
||||
visibility: :private
|
||||
})
|
||||
|> Notes.create_note(user)
|
||||
|
@ -3,19 +3,24 @@ 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.
|
||||
"""
|
||||
def pipeline_fixture(attrs \\ %{}) do
|
||||
@spec pipeline_fixture(User.t()) :: Pipeline.t()
|
||||
@spec pipeline_fixture(attrs :: map(), User.t()) :: Pipeline.t()
|
||||
def pipeline_fixture(attrs \\ %{}, user) do
|
||||
{:ok, pipeline} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
description: "some description",
|
||||
title: "some title",
|
||||
visibility: :public
|
||||
tag: [],
|
||||
slug: random_slug(),
|
||||
visibility: :private
|
||||
})
|
||||
|> Memex.Pipelines.create_pipeline()
|
||||
|> Pipelines.create_pipeline(user)
|
||||
|
||||
pipeline
|
||||
end
|
||||
|
@ -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 \\ %{}) do
|
||||
def step_fixture(attrs \\ %{}, position, pipeline, user) do
|
||||
{:ok, step} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
description: "some description",
|
||||
position: 42,
|
||||
content: "some content",
|
||||
title: "some title"
|
||||
})
|
||||
|> Memex.Steps.create_step()
|
||||
|> Steps.create_step(position, pipeline, user)
|
||||
|
||||
step
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user