5 Commits

Author SHA1 Message Date
f03037a943 add search to notes
Some checks are pending
continuous-integration/drone/push Build is pending
2022-11-18 23:45:24 -05:00
282d2b7664 remove transition all on input 2022-11-18 23:45:24 -05:00
3dbbb7e21c alias endpoint 2022-11-18 23:45:24 -05:00
b641e96601 display topbar when user is logged out 2022-11-18 23:45:24 -05:00
e0f0e39326 work on notes 2022-11-18 23:45:24 -05:00
111 changed files with 1525 additions and 5824 deletions

View File

@ -16,7 +16,7 @@ steps:
- assets/node_modules/ - assets/node_modules/
- name: test - name: test
image: elixir:1.14.1-alpine image: elixir:1.13.4-alpine
environment: environment:
TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test TEST_DATABASE_URL: ecto://postgres:postgres@database/memex_test
HOST: testing.example.tld HOST: testing.example.tld
@ -29,7 +29,7 @@ steps:
- npm --prefix ./assets ci --progress=false --no-audit --loglevel=error - npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
- npm run --prefix ./assets deploy - npm run --prefix ./assets deploy
- mix do phx.digest, gettext.extract - mix do phx.digest, gettext.extract
- mix test.all - mix test
- name: build and publish stable - name: build and publish stable
image: thegeeklab/drone-docker-buildx image: thegeeklab/drone-docker-buildx
@ -38,7 +38,7 @@ steps:
repo: shibaobun/memex repo: shibaobun/memex
purge: true purge: true
compress: true compress: true
platforms: linux/amd64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
username: username:
from_secret: docker_username from_secret: docker_username
password: password:
@ -55,7 +55,7 @@ steps:
repo: shibaobun/memex repo: shibaobun/memex
purge: true purge: true
compress: true compress: true
platforms: linux/amd64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
username: username:
from_secret: docker_username from_secret: docker_username
password: password:

View File

@ -1,4 +1,4 @@
FROM elixir:1.14.1-alpine AS build FROM elixir:1.13-alpine AS build
# install build dependencies # install build dependencies
RUN apk add --no-cache build-base npm git python3 RUN apk add --no-cache build-base npm git python3
@ -37,7 +37,7 @@ RUN mix do compile, release
FROM alpine:latest AS app FROM alpine:latest AS app
RUN apk upgrade --no-cache && \ RUN apk upgrade --no-cache && \
apk add --no-cache bash openssl libssl1.1 libcrypto1.1 libgcc libstdc++ ncurses-libs apk add --no-cache bash openssl libgcc libstdc++ ncurses-libs
WORKDIR /app WORKDIR /app

View File

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

View File

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

View File

@ -1,17 +0,0 @@
# v0.1.3
- backlink to other notes in notes
- search tags on click
# v0.1.2
- fix more typos
- add to faq
- check for slug uniqueness before submitting
# v0.1.1
- improve search a whole lot
- improve table information for notes and contexts
- fix some typos
- use project version on homepage
# v0.1.0
- initial release >:3c

View File

@ -62,7 +62,7 @@ if config_env() == :prod do
System.get_env("SECRET_KEY_BASE") || System.get_env("SECRET_KEY_BASE") ||
raise """ raise """
environment variable SECRET_KEY_BASE is missing. environment variable SECRET_KEY_BASE is missing.
You can generate one by running: mix phx.gen.secret You can generate one by calling: mix phx.gen.secret
""" """
config :memex, MemexWeb.Endpoint, secret_key_base: secret_key_base config :memex, MemexWeb.Endpoint, secret_key_base: secret_key_base
@ -79,7 +79,7 @@ if config_env() == :prod do
password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"), password: System.get_env("SMTP_PASSWORD") || raise("No SMTP_PASSWORD set!"),
ssl: System.get_env("SMTP_SSL") == "true", ssl: System.get_env("SMTP_SSL") == "true",
email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}", email_from: System.get_env("EMAIL_FROM") || "no-reply@#{System.get_env("HOST")}",
email_name: System.get_env("EMAIL_NAME") || "memEx" email_name: System.get_env("EMAIL_NAME") || "Memex"
# ## Using releases # ## Using releases
# #

View File

@ -77,14 +77,14 @@ Check them out!
For development, I recommend setting environment variables with For development, I recommend setting environment variables with
[direnv](https://direnv.net). [direnv](https://direnv.net).
By default, memEx will always bind to all external IPv4 and IPv6 addresses in By default, Memex will always bind to all external IPv4 and IPv6 addresses in
`dev` and `prod` mode, respectively. If you would like to use different values, `dev` and `prod` mode, respectively. If you would like to use different values,
they will need to be overridden in `config/dev.exs` and `config/runtime.exs` for they will need to be overridden in `config/dev.exs` and `config/runtime.exs` for
`dev` and `prod` modes, respectively. `dev` and `prod` modes, respectively.
## `MIX_ENV=dev` ## `MIX_ENV=dev`
In `dev` mode, memEx will listen for these environment variables at runtime. In `dev` mode, Memex will listen for these environment variables at runtime.
- `HOST`: External url to generate links with. Set this especially if you're - `HOST`: External url to generate links with. Set this especially if you're
behind a reverse proxy. Defaults to `localhost`. External URLs will always be behind a reverse proxy. Defaults to `localhost`. External URLs will always be
@ -100,7 +100,7 @@ In `dev` mode, memEx will listen for these environment variables at runtime.
## `MIX_ENV=test` ## `MIX_ENV=test`
In `test` mode (or in the Docker container), memEx will listen for the same environment variables as dev mode, but also include the following at runtime: In `test` mode (or in the Docker container), Memex will listen for the same environment variables as dev mode, but also include the following at runtime:
- `TEST_DATABASE_URL`: REPLACES `DATABASE_URL`. Controls the database url to - `TEST_DATABASE_URL`: REPLACES `DATABASE_URL`. Controls the database url to
connect to. Defaults to `ecto://postgres:postgres@localhost/memex_test`. connect to. Defaults to `ecto://postgres:postgres@localhost/memex_test`.
@ -110,7 +110,7 @@ In `test` mode (or in the Docker container), memEx will listen for the same envi
## `MIX_ENV=prod` ## `MIX_ENV=prod`
In `prod` mode (or in the Docker container), memEx will listen for the same environment variables as dev mode, but also include the following at runtime: In `prod` mode (or in the Docker container), Memex will listen for the same environment variables as dev mode, but also include the following at runtime:
- `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated - `SECRET_KEY_BASE`: Secret key base used to sign cookies. Must be generated
with `docker run -it shibaobun/memex mix phx.gen.secret` and set for server to start. with `docker run -it shibaobun/memex mix phx.gen.secret` and set for server to start.
@ -121,4 +121,4 @@ In `prod` mode (or in the Docker container), memEx will listen for the same envi
- `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`. - `SMTP_SSL`: Set to `true` to enable SSL for emails. Defaults to `false`.
- `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to - `EMAIL_FROM`: Sets the sender email in sent emails. Defaults to
`no-reply@HOST` where `HOST` was previously defined. `no-reply@HOST` where `HOST` was previously defined.
- `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "memEx". - `EMAIL_NAME`: Sets the sender name in sent emails. Defaults to "Memex".

View File

@ -25,8 +25,8 @@ services:
# - SMTP_SSL=false # - SMTP_SSL=false
# optional, default is format below # optional, default is format below
# - EMAIL_FROM=no-reply@memex.example.tld # - EMAIL_FROM=no-reply@memex.example.tld
# optional, default is "memEx" # optional, default is "Memex"
# - EMAIL_NAME=memEx # - EMAIL_NAME=Memex
expose: expose:
- "4000" - "4000"
depends_on: depends_on:

BIN
home.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

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

View File

@ -19,8 +19,8 @@ defmodule Memex.Email do
@spec base_email(User.t(), String.t()) :: t() @spec base_email(User.t(), String.t()) :: t()
defp base_email(%User{email: email}, subject) do defp base_email(%User{email: email}, subject) do
from = Application.get_env(:memex, Memex.Mailer)[:email_from] || "noreply@localhost" from = Application.get_env(:Memex, Memex.Mailer)[:email_from] || "noreply@localhost"
name = Application.get_env(:memex, Memex.Mailer)[:email_name] name = Application.get_env(:Memex, Memex.Mailer)[:email_name]
new() |> to(email) |> from({name, from}) |> subject(subject) new() |> to(email) |> from({name, from}) |> subject(subject)
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -1,132 +0,0 @@
defmodule MemexWeb.Components.ContextsTableComponent do
@moduledoc """
A component that displays a list of contexts
"""
use MemexWeb, :live_component
alias Ecto.UUID
alias Memex.{Accounts.User, Contexts.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("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(:tags, %{tags: tags}, _additional_data) do
assigns = %{tags: tags}
~H"""
<div class="flex flex-wrap justify-center space-x-1">
<%= for tag <- @tags do %>
<.link patch={Routes.context_index_path(Endpoint, :search, tag)} class="link">
<%= tag %>
</.link>
<% end %>
</div>
"""
end
defp get_value_for_key(:actions, context, %{actions: actions}) do
assigns = %{actions: actions, context: context}
~H"""
<div class="flex justify-center items-center space-x-4">
<%= render_slot(@actions, @context) %>
</div>
"""
end
defp get_value_for_key(key, context, _additional_data), do: context |> Map.get(key)
end

View File

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

View File

@ -1,44 +0,0 @@
defmodule MemexWeb.Components.NoteContent do
@moduledoc """
Display the content for a note
"""
use MemexWeb, :component
alias Memex.Notes.Note
alias Phoenix.HTML
attr :note, Note, required: true
def note_content(assigns) do
~H"""
<div
id={"show-note-content-#{@note.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(@note.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: "note-link-#{slug}"]
)
|> HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
"</p>#{link}<p class=\"inline\">"
end
)
|> HTML.raw()
end
end

View File

@ -4,7 +4,7 @@ defmodule MemexWeb.Components.NotesTableComponent do
""" """
use MemexWeb, :live_component use MemexWeb, :live_component
alias Ecto.UUID alias Ecto.UUID
alias Memex.{Accounts.User, Notes.Note} alias Memex.{Accounts.User, Notes, Notes.Note}
alias Phoenix.LiveView.{Rendered, Socket} alias Phoenix.LiveView.{Rendered, Socket}
@impl true @impl true
@ -44,7 +44,8 @@ defmodule MemexWeb.Components.NotesTableComponent do
end end
columns = [ columns = [
%{label: gettext("slug"), key: :slug}, %{label: gettext("title"), key: :title},
%{label: gettext("content"), key: :content},
%{label: gettext("tags"), key: :tags}, %{label: gettext("tags"), key: :tags},
%{label: gettext("visibility"), key: :visibility} %{label: gettext("visibility"), key: :visibility}
| columns | columns
@ -88,34 +89,36 @@ defmodule MemexWeb.Components.NotesTableComponent do
@spec get_value_for_key(atom(), Note.t(), additional_data :: map()) :: @spec get_value_for_key(atom(), Note.t(), additional_data :: map()) ::
any() | {any(), Rendered.t()} any() | {any(), Rendered.t()}
defp get_value_for_key(:slug, %{slug: slug}, _additional_data) do defp get_value_for_key(:title, %{id: id, title: title}, _additional_data) do
assigns = %{slug: slug} assigns = %{id: id, title: title}
slug_block = ~H""" title_block = ~H"""
<.link <.link
navigate={Routes.note_show_path(Endpoint, :show, @slug)} navigate={Routes.note_show_path(Endpoint, :show, @id)}
class="link" class="link"
data-qa={"note-show-#{@slug}"} data-qa={"note-show-#{@id}"}
> >
<%= @slug %> <%= @title %>
</.link> </.link>
""" """
{slug, slug_block} {title, title_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 end
defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do defp get_value_for_key(:tags, %{tags: tags}, _additional_data) do
assigns = %{tags: tags} tags |> Notes.get_tags_string()
~H"""
<div class="flex flex-wrap justify-center space-x-1">
<%= for tag <- @tags do %>
<.link patch={Routes.note_index_path(Endpoint, :search, tag)} class="link">
<%= tag %>
</.link>
<% end %>
</div>
"""
end end
defp get_value_for_key(:actions, note, %{actions: actions}) do defp get_value_for_key(:actions, note, %{actions: actions}) do

View File

@ -1,145 +0,0 @@
defmodule MemexWeb.Components.PipelinesTableComponent do
@moduledoc """
A component that displays a list of pipelines
"""
use MemexWeb, :live_component
alias Ecto.UUID
alias Memex.{Accounts.User, Pipelines.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
assigns = %{tags: tags}
~H"""
<div class="flex flex-wrap justify-center space-x-1">
<%= for tag <- @tags do %>
<.link patch={Routes.pipeline_index_path(Endpoint, :search, tag)} class="link">
<%= tag %>
</.link>
<% end %>
</div>
"""
end
defp get_value_for_key(:actions, pipeline, %{actions: actions}) do
assigns = %{actions: actions, pipeline: pipeline}
~H"""
<div class="flex justify-center items-center space-x-4">
<%= render_slot(@actions, @pipeline) %>
</div>
"""
end
defp get_value_for_key(key, pipeline, _additional_data), do: pipeline |> Map.get(key)
end

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ defmodule MemexWeb.UserResetPasswordController do
plug :get_user_by_reset_password_token when action in [:edit, :update] plug :get_user_by_reset_password_token when action in [:edit, :update]
def new(conn, _params) do def new(conn, _params) do
render(conn, "new.html", page_title: gettext("forgot your password?")) render(conn, "new.html", page_title: gettext("Forgot your password?"))
end end
def create(conn, %{"user" => %{"email" => email}}) do def create(conn, %{"user" => %{"email" => email}}) do

View File

@ -20,7 +20,7 @@ defmodule MemexWeb.UserSessionController do
def delete(conn, _params) do def delete(conn, _params) do
conn conn
|> put_flash(:info, dgettext("prompts", "logged out successfully.")) |> put_flash(:info, dgettext("prompts", "Logged out successfully."))
|> UserAuth.log_out_user() |> UserAuth.log_out_user()
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,37 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl"> <div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl">
<h1 class="text-xl"> <h1 class="text-xl">
<%= @note.slug %> <%= @note.title %>
</h1> </h1>
<div class="flex flex-wrap space-x-1"> <p><%= if @note.tags, do: @note.tags |> Enum.join(", ") %></p>
<%= for tag <- @note.tags do %>
<.link navigate={Routes.note_index_path(Endpoint, :search, tag)} class="link">
<%= tag %>
</.link>
<% end %>
</div>
<.note_content note={@note} /> <textarea
id="show-note-content"
class="input input-primary h-128 min-h-128"
phx-hook="MaintainAttrs"
phx-update="ignore"
readonly
phx-no-format
><%= @note.content %></textarea>
<p class="self-end"> <p class="self-end">
<%= gettext("Visibility: %{visibility}", visibility: @note.visibility) %> <%= gettext("Visibility: %{visibility}", visibility: @note.visibility) %>
</p> </p>
<div class="self-end flex space-x-4"> <div class="self-end flex space-x-4">
<.link class="btn btn-primary" navigate={Routes.note_index_path(@socket, :index)}> <.link class="btn btn-primary" patch={Routes.note_index_path(@socket, :index)}>
<%= dgettext("actions", "back") %> <%= dgettext("actions", "Back") %>
</.link> </.link>
<%= if is_owner?(@note, @current_user) do %> <%= if @current_user do %>
<.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note.slug)}> <.link class="btn btn-primary" patch={Routes.note_show_path(@socket, :edit, @note)}>
<%= dgettext("actions", "edit") %> <%= dgettext("actions", "edit") %>
</.link> </.link>
<% end %> <% end %>
<%= if is_owner_or_admin?(@note, @current_user) do %>
<button
type="button"
class="btn btn-primary"
phx-click="delete"
data-confirm={dgettext("prompts", "are you sure?")}
data-qa={"delete-note-#{@note.id}"}
>
<%= dgettext("actions", "delete") %>
</button>
<% end %>
</div> </div>
</div> </div>
<%= if @live_action in [:edit] do %> <%= if @live_action in [:edit] do %>
<.modal return_to={Routes.note_show_path(@socket, :show, @note.slug)}> <.modal return_to={Routes.note_show_path(@socket, :show, @note)}>
<.live_component <.live_component
module={MemexWeb.NoteLive.FormComponent} module={MemexWeb.NoteLive.FormComponent}
id={@note.id} id={@note.id}
@ -49,7 +39,7 @@
title={@page_title} title={@page_title}
action={@live_action} action={@live_action}
note={@note} note={@note}
return_to={Routes.note_show_path(@socket, :show, @note.slug)} return_to={Routes.note_show_path(@socket, :show, @note)}
/> />
</.modal> </.modal>
<% end %> <% end %>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,180 +1,43 @@
<div class="mx-auto flex flex-col justify-center items-stretch space-y-4 max-w-3xl"> <h1>show pipeline</h1>
<h1 class="text-xl">
<%= @pipeline.slug %>
</h1>
<div class="flex flex-wrap space-x-1"> <%= if @live_action in [:edit] do %>
<%= for tag <- @pipeline.tags do %> <.modal return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)}>
<.link navigate={Routes.pipeline_index_path(Endpoint, :search, tag)} class="link"> <.live_component
<%= tag %> module={MemexWeb.PipelineLive.FormComponent}
</.link> id={@pipeline.id}
<% end %> title={@page_title}
</div> action={@live_action}
pipeline={@pipeline}
<%= if @pipeline.description do %> return_to={Routes.pipeline_show_path(@socket, :show, @pipeline)}
<textarea />
id="show-pipeline-description" </.modal>
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 %> <% end %>
<ul>
<li>
<strong>Title:</strong>
<%= @pipeline.title %>
</li>
<li>
<strong>Description:</strong>
<%= @pipeline.description %>
</li>
<li>
<strong>Visibility:</strong>
<%= @pipeline.visibility %>
</li>
</ul>
<span>
<.link patch={Routes.pipeline_show_path(@socket, :edit, @pipeline)} class="button">
<%= dgettext("actions", "edit") %>
</.link>
</span>
|
<span>
<.link patch={Routes.pipeline_index_path(@socket, :index)}>
<%= dgettext("actions", "Back") %>
</.link>
</span>

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
<br /> <br />
<span style="margin-bottom: 1em; font-size: 1.25em;"> <span style="margin-bottom: 1em; font-size: 1.25em;">
<%= dgettext("emails", "Welcome to memEx") %> <%= dgettext("emails", "Welcome to Memex") %>
</span> </span>
<br /> <br />
@ -19,5 +19,5 @@
<br /> <br />
<%= dgettext("emails", "If you didn't create an account at memEx, please ignore this.") %> <%= dgettext("emails", "If you didn't create an account at Memex, please ignore this.") %>
</div> </div>

View File

@ -1,7 +1,7 @@
<%= dgettext("emails", "Hi %{email},", email: @user.email) %> <%= dgettext("emails", "Hi %{email},", email: @user.email) %>
<%= dgettext("emails", "Welcome to memEx") %> <%= dgettext("emails", "Welcome to Memex") %>
<%= dgettext("emails", "You can confirm your account by visiting the URL below:") %> <%= dgettext("emails", "You can confirm your account by visiting the URL below:") %>

View File

@ -13,5 +13,5 @@
<br /> <br />
<%= dgettext("emails", "If you didn't request this change from memEx, please ignore this.") %> <%= dgettext("emails", "If you didn't request this change from Memex, please ignore this.") %>
</div> </div>

View File

@ -15,6 +15,6 @@
<%= dgettext( <%= dgettext(
"emails", "emails",
"If you didn't request this change from memEx, please ignore this." "If you didn't request this change from Memex, please ignore this."
) %> ) %>
</div> </div>

View File

@ -5,13 +5,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title> <title>
<%= dgettext("errors", "Error") %> | memEx <%= dgettext("errors", "Error") %>| Memex
</title> </title>
<link rel="stylesheet" href="/css/app.css" /> <link rel="stylesheet" href="/css/app.css" />
<script defer type="text/javascript" src="/js/app.js"> <script defer type="text/javascript" src="/js/app.js">
</script> </script>
</head> </head>
<body class="m-0 p-0 w-full h-full bg-primary-800 text-primary-400 subpixel-antialiased"> <body class="pb-8 m-0 p-0 w-full h-full">
<header> <header>
<.topbar current_user={assigns[:current_user]}></.topbar> <.topbar current_user={assigns[:current_user]}></.topbar>
</header> </header>
@ -25,7 +25,7 @@
<hr class="w-full hr" /> <hr class="w-full hr" />
<a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg"> <a href={Routes.live_path(Endpoint, HomeLive)} class="link title text-primary-400 text-lg">
<%= dgettext("errors", "go back home") %> <%= dgettext("errors", "Go back home") %>
</a> </a>
</div> </div>
</div> </div>

View File

@ -4,15 +4,15 @@
<%= @email.subject %> <%= @email.subject %>
</title> </title>
</head> </head>
<body style="padding: 2em; color: rgb(161, 161, 170); background-color: rgb(39, 39, 42); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;"> <body style="padding: 2em; color: rgb(31, 31, 31); background-color: rgb(220, 220, 228); font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; text-align: center;">
<%= @inner_content %> <%= @inner_content %>
<hr style="margin: 2em auto; border-width: 1px; border-color: rgb(161, 161, 170); width: 100%; max-width: 42rem;" /> <hr style="margin: 2em auto; border-width: 1px; border-color: rgb(212, 212, 216); width: 100%; max-width: 42rem;" />
<a style="color: rgb(161, 161, 170);" href={Routes.live_url(Endpoint, HomeLive)}> <a style="color: rgb(31, 31, 31);" href={Routes.live_url(Endpoint, HomeLive)}>
<%= dgettext( <%= dgettext(
"emails", "emails",
"This email was sent from memEx" "This email was sent from Memex, the self-hosted firearm tracker website."
) %> ) %>
</a> </a>
</body> </body>

View File

@ -7,5 +7,5 @@
===================== =====================
<%= dgettext("emails", <%= dgettext("emails",
"This email was sent from memEx at %{url}", "This email was sent from Memex at %{url}, the self-hosted firearm tracker website.",
url: Routes.live_url(Endpoint, HomeLive)) %> url: Routes.live_url(Endpoint, HomeLive)) %>

View File

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

View File

@ -33,7 +33,7 @@
<%= select( <%= select(
f, f,
:locale, :locale,
[{gettext("english"), "en_US"}], [{gettext("English"), "en_US"}],
class: "input input-primary col-span-2" class: "input input-primary col-span-2"
) %> ) %>
<%= error_tag(f, :locale) %> <%= error_tag(f, :locale) %>
@ -48,7 +48,7 @@
<%= dgettext("actions", "log in") %> <%= dgettext("actions", "log in") %>
</.link> </.link>
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary"> <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
<%= dgettext("actions", "forgot your password?") %> <%= dgettext("actions", "Forgot your password?") %>
</.link> </.link>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4"> <div class="mx-auto mb-8 max-w-2xl flex flex-col justify-center items-center space-y-4">
<h1 class="title text-primary-400 text-xl"> <h1 class="title text-primary-400 text-xl">
<%= dgettext("actions", "forgot your password?") %> <%= dgettext("actions", "Forgot your password?") %>
</h1> </h1>
<.form <.form
@ -12,7 +12,7 @@
<%= label(f, :email, class: "title text-lg text-primary-400") %> <%= label(f, :email, class: "title text-lg text-primary-400") %>
<%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %> <%= email_input(f, :email, required: true, class: "input input-primary col-span-2") %>
<%= submit(dgettext("actions", "send instructions to reset password"), <%= submit(dgettext("actions", "Send instructions to reset password"),
class: "mx-auto btn btn-primary col-span-3" class: "mx-auto btn btn-primary col-span-3"
) %> ) %>
</.form> </.form>

View File

@ -41,7 +41,7 @@
</.link> </.link>
<% end %> <% end %>
<.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary"> <.link href={Routes.user_reset_password_path(@conn, :new)} class="btn btn-primary">
<%= dgettext("actions", "forgot your password?") %> <%= dgettext("actions", "Forgot your password?") %>
</.link> </.link>
</div> </div>
</div> </div>

View File

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

View File

@ -1,12 +1,17 @@
defmodule MemexWeb.LayoutView do defmodule MemexWeb.LayoutView do
use MemexWeb, :view use MemexWeb, :view
import MemexWeb.{Components.Topbar, Gettext} import MemexWeb.Components.Topbar
alias MemexWeb.HomeLive alias MemexWeb.HomeLive
# Phoenix LiveDashboard is available only in development by default, # Phoenix LiveDashboard is available only in development by default,
# so we instruct Elixir to not warn if the dashboard route is missing. # so we instruct Elixir to not warn if the dashboard route is missing.
@compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}}
def get_title(%{assigns: %{title: title}}), do: gettext("memEx | %{title}", title: title) def get_title(conn) do
def get_title(_conn), do: gettext("memEx") if conn.assigns |> Map.has_key?(:title) do
"Memex | #{conn.assigns.title}"
else
"Memex"
end
end
end end

View File

@ -4,7 +4,7 @@ defmodule Memex.MixProject do
def project do def project do
[ [
app: :memex, app: :memex,
version: "0.1.3", version: "0.1.0",
elixir: "~> 1.14", elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(), compilers: Mix.compilers(),
@ -15,9 +15,9 @@ defmodule Memex.MixProject do
consolidate_protocols: Mix.env() not in [:dev, :test], consolidate_protocols: Mix.env() not in [:dev, :test],
preferred_cli_env: [test: :test, "test.all": :test], preferred_cli_env: [test: :test, "test.all": :test],
# ExDoc # ExDoc
name: "memEx", name: "memex",
source_url: "https://gitea.bubbletea.dev/shibao/memEx", source_url: "https://gitea.bubbletea.dev/shibao/memex",
homepage_url: "https://gitea.bubbletea.dev/shibao/memEx", homepage_url: "https://gitea.bubbletea.dev/shibao/memex",
docs: [ docs: [
# The main page in the docs # The main page in the docs
main: "README.md", main: "README.md",

View File

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

View File

@ -1,158 +0,0 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2\n"
#: lib/memex_web/live/invite_live/index.html.heex:30
#, elixir-autogen, elixir-format
msgid "Copy to clipboard"
msgstr ""
#: lib/memex_web/templates/user_confirmation/new.html.heex:3
#: lib/memex_web/templates/user_confirmation/new.html.heex:15
#, elixir-autogen, elixir-format
msgid "Resend confirmation instructions"
msgstr ""
#: lib/memex_web/templates/user_reset_password/edit.html.heex:3
#: lib/memex_web/templates/user_reset_password/edit.html.heex:33
#, elixir-autogen, elixir-format
msgid "Reset password"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:28
#, elixir-autogen, elixir-format
msgid "Save"
msgstr ""
#: lib/memex_web/templates/user_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:40
#: lib/memex_web/live/note_live/index.html.heex:49
#: lib/memex_web/live/note_live/show.html.heex:37
#: lib/memex_web/live/pipeline_live/index.html.heex:49
#: lib/memex_web/live/pipeline_live/show.html.heex:49
#: lib/memex_web/live/pipeline_live/show.html.heex:119
#, 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:29
#: lib/memex_web/live/note_live/index.html.heex:38
#: lib/memex_web/live/note_live/show.html.heex:26
#: lib/memex_web/live/pipeline_live/index.html.heex:38
#: lib/memex_web/live/pipeline_live/show.html.heex:38
#: lib/memex_web/live/pipeline_live/show.html.heex:108
#, 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: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
#, elixir-autogen, elixir-format
msgid "log in"
msgstr ""
#: lib/memex_web/live/context_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new context"
msgstr ""
#: lib/memex_web/live/note_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new note"
msgstr ""
#: lib/memex_web/live/pipeline_live/index.html.heex:58
#, elixir-autogen, elixir-format
msgid "new pipeline"
msgstr ""
#: lib/memex_web/components/topbar.ex:115
#: lib/memex_web/templates/user_confirmation/new.html.heex:25
#: lib/memex_web/templates/user_registration/new.html.heex:3
#: lib/memex_web/templates/user_registration/new.html.heex:41
#: lib/memex_web/templates/user_reset_password/edit.html.heex:43
#: lib/memex_web/templates/user_reset_password/new.html.heex:25
#: lib/memex_web/templates/user_session/new.html.heex:40
#, elixir-autogen, elixir-format
msgid "register"
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:42
#: lib/memex_web/live/note_live/form_component.html.heex:42
#: lib/memex_web/live/pipeline_live/form_component.html.heex:42
#: lib/memex_web/live/step_live/form_component.html.heex:28
#, elixir-autogen, elixir-format
msgid "save"
msgstr ""
#: lib/memex_web/live/context_live/show.html.heex:22
#: lib/memex_web/live/note_live/show.html.heex:22
#: lib/memex_web/live/pipeline_live/show.html.heex:31
#, elixir-autogen, elixir-format
msgid "back"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:135
#, elixir-autogen, elixir-format
msgid "add step"
msgstr ""
#: 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, fuzzy
msgid "forgot your password?"
msgstr ""
#: lib/memex_web/templates/user_reset_password/new.html.heex:15
#, elixir-autogen, elixir-format, fuzzy
msgid "send instructions to reset password"
msgstr ""

View File

@ -1,667 +0,0 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-11-27 04:49+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 3.7.4\n"
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here has no
## effect: edit them in PO (.po) files instead.
#: lib/memex_web/live/invite_live/index.html.heex:90
#, elixir-autogen, elixir-format
msgid "Admins"
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:8
#, elixir-autogen, elixir-format
msgid "Confirm your account"
msgstr ""
#: lib/memex_web/templates/user_session/new.html.heex:27
#, elixir-autogen, elixir-format
msgid "Keep me logged in for 60 days"
msgstr ""
#: lib/memex_web/templates/user_registration/new.html.heex:32
#, elixir-autogen, elixir-format
msgid "Language"
msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:37
#, elixir-autogen, elixir-format
msgid "Loading..."
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:20
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:50
#, elixir-autogen, elixir-format
msgid "Reconnecting..."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:36
#, elixir-autogen, elixir-format
msgid "Reset your password"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:10
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/memex_web/components/user_card.ex:33
#, elixir-autogen, elixir-format
msgid "User registered on"
msgstr ""
#: lib/memex_web/components/invite_card.ex:19
#, elixir-autogen, elixir-format
msgid "Uses Left:"
msgstr ""
#: 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:17
#: lib/memex_web/live/note_live/show.html.heex:17
#: lib/memex_web/live/pipeline_live/show.html.heex:26
#, 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/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_registration/new.html.heex:36
#: 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:48
#: lib/memex_web/components/notes_table_component.ex:48
#: 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:49
#: lib/memex_web/components/notes_table_component.ex:49
#: 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:15
#, 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
#: lib/memex_web/views/layout_view.ex:11
#, 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:68
#, 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:62
#, elixir-autogen, elixir-format
msgid "no steps"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:57
#, elixir-autogen, elixir-format
msgid "steps:"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:14
#, elixir-autogen, elixir-format
msgid "title"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "use [[context-slug]] to link to a context"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:72
#, elixir-autogen, elixir-format
msgid "finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:102
#, elixir-autogen, elixir-format
msgid "for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:118
#, elixir-autogen, elixir-format
msgid "for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:62
#, elixir-autogen, elixir-format
msgid "i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:67
#, elixir-autogen, elixir-format
msgid "i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:99
#, elixir-autogen, elixir-format
msgid "in my opinion, contexts should be like single-topic blog posts."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:83
#, elixir-autogen, elixir-format
msgid "in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:113
#, elixir-autogen, elixir-format
msgid "in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:31
#, elixir-autogen, elixir-format
msgid "memex"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:51
#, elixir-autogen, elixir-format
msgid "org-mode"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:20
#, elixir-autogen, elixir-format
msgid "some things that this memex is very loosely inspired by:"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:88
#, elixir-autogen, elixir-format
msgid "spoons? probably not. a particular brand of spoons that you really like? why not :)"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:14
#, elixir-autogen, elixir-format
msgid "this is a memex, used to document not just your notes, but also your perspectives and processes."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:96
#, elixir-autogen, elixir-format
msgid "what should my contexts be like?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:80
#, elixir-autogen, elixir-format
msgid "what should my notes be like?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:110
#, elixir-autogen, elixir-format
msgid "what should my pipelines be like?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:59
#, elixir-autogen, elixir-format
msgid "why split up into notes, contexts and pipelines?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:41
#, elixir-autogen, elixir-format
msgid "zettelkasten"
msgstr ""
#: lib/memex_web/views/layout_view.ex:10
#, elixir-autogen, elixir-format
msgid "memEx | %{title}"
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:9
#, elixir-autogen, elixir-format, fuzzy
msgid "forgot your password?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:126
#, elixir-autogen, elixir-format
msgid "how many people should i invite?"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:134
#, elixir-autogen, elixir-format
msgid "note, context and pipeline slugs must be unique, and you are free to backlink to notes not written by you."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:139
#, elixir-autogen, elixir-format
msgid "so, i'd recommend inviting anyone you'd like to work on your collective memEx. however, when in doubt, hopefully setting up a new instance is easy enough. if it isn't, then feel free to let me know :)"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:129
#, elixir-autogen, elixir-format
msgid "while memEx fully supports multiple users, each memEx instance should be treated as a single cohesive and collaborative document."
msgstr ""

View File

@ -1,93 +0,0 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2\n"
#: lib/memex/accounts/email.ex:30
#, elixir-autogen, elixir-format
msgid "Confirm your Memex account"
msgstr ""
#: lib/memex_web/templates/email/confirm_email.html.heex:3
#: lib/memex_web/templates/email/confirm_email.txt.eex:2
#: lib/memex_web/templates/email/reset_password.html.heex:3
#: lib/memex_web/templates/email/reset_password.txt.eex:2
#: lib/memex_web/templates/email/update_email.html.heex:3
#: lib/memex_web/templates/email/update_email.txt.eex:2
#, elixir-autogen, elixir-format
msgid "Hi %{email},"
msgstr ""
#: lib/memex_web/templates/email/confirm_email.txt.eex:10
#, elixir-autogen, elixir-format
msgid "If you didn't create an account at %{url}, please ignore this."
msgstr ""
#: 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 ""
#: lib/memex/accounts/email.ex:37
#, elixir-autogen, elixir-format
msgid "Reset your Memex password"
msgstr ""
#: lib/memex/accounts/email.ex:44
#, elixir-autogen, elixir-format
msgid "Update your Memex email"
msgstr ""
#: 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 ""
#: 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 ""
#: 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 ""
#: lib/memex_web/templates/layout/email.html.heex:13
#, elixir-autogen, elixir-format
msgid "This email was sent from memEx"
msgstr ""
#: lib/memex_web/templates/layout/email.txt.eex:9
#, elixir-autogen, elixir-format
msgid "This email was sent from memEx at %{url}"
msgstr ""
#: lib/memex_web/templates/email/confirm_email.html.heex:22
#, elixir-autogen, elixir-format, fuzzy
msgid "If you didn't create an account at memEx, please ignore this."
msgstr ""
#: lib/memex_web/templates/email/reset_password.html.heex:16
#: lib/memex_web/templates/email/update_email.html.heex:16
#, elixir-autogen, elixir-format, fuzzy
msgid "If you didn't request this change from memEx, please ignore this."
msgstr ""
#: lib/memex_web/templates/email/confirm_email.html.heex:9
#: lib/memex_web/templates/email/confirm_email.txt.eex:4
#, elixir-autogen, elixir-format, fuzzy
msgid "Welcome to memEx"
msgstr ""

View File

@ -1,133 +0,0 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2\n"
#: lib/memex_web/controllers/user_settings_controller.ex:84
#, elixir-autogen, elixir-format
msgid "Email change link is invalid or it has expired."
msgstr ""
#: lib/memex_web/templates/error/error.html.heex:8
#, elixir-autogen, elixir-format
msgid "Error"
msgstr ""
#: lib/memex_web/controllers/user_session_controller.ex:17
#, elixir-autogen, elixir-format
msgid "Invalid email or password"
msgstr ""
#: 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:64
#, elixir-autogen, elixir-format
msgid "Oops, something went wrong! Please check the errors below."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:63
#, elixir-autogen, elixir-format
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
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
msgid "Sorry, this invite was not found or expired"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:99
#, elixir-autogen, elixir-format
msgid "Unable to delete user"
msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:54
#, elixir-autogen, elixir-format
msgid "User confirmation link is invalid or it has expired."
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:18
#, elixir-autogen, elixir-format
msgid "You are not authorized to view this page"
msgstr ""
#: lib/memex_web/controllers/user_auth.ex:177
#, elixir-autogen, elixir-format
msgid "You are not authorized to view this page."
msgstr ""
#: 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
msgid "did not change"
msgstr ""
#: lib/memex/accounts/user.ex:150
#, elixir-autogen, elixir-format
msgid "does not match password"
msgstr ""
#: lib/memex/accounts/user.ex:187
#, elixir-autogen, elixir-format
msgid "is not valid"
msgstr ""
#: lib/memex/accounts/user.ex:85
#, elixir-autogen, elixir-format
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:62
#: lib/memex/notes/note.ex:48
#: lib/memex/notes/note.ex:61
#: lib/memex/pipelines/pipeline.ex:50
#: lib/memex/pipelines/pipeline.ex:63
#, elixir-autogen, elixir-format
msgid "invalid format: only numbers, letters and hyphen are accepted"
msgstr ""
#: lib/memex_web/templates/error/error.html.heex:28
#, elixir-autogen, elixir-format
msgid "go back home"
msgstr ""
#: lib/memex_web/views/error_view.ex:11
#, elixir-autogen, elixir-format
msgid "internal server error"
msgstr ""
#: lib/memex_web/views/error_view.ex:9
#, elixir-autogen, elixir-format
msgid "not found"
msgstr ""
#: lib/memex_web/views/error_view.ex:10
#, elixir-autogen, elixir-format
msgid "unauthorized"
msgstr ""

View File

@ -1,159 +0,0 @@
## "msgid"s in this file come from POT (.pot) files.
##
## Do not add, change, or remove "msgid"s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use "mix gettext.extract --merge" or "mix gettext.merge"
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: de\n"
"Plural-Forms: nplurals=2\n"
#: lib/memex_web/controllers/user_confirmation_controller.ex:38
#, elixir-autogen, elixir-format
msgid "%{email} confirmed successfully."
msgstr ""
#: lib/memex_web/live/invite_live/form_component.ex:62
#, elixir-autogen, elixir-format
msgid "%{invite_name} created successfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "%{invite_name} deleted succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:114
#, elixir-autogen, elixir-format
msgid "%{invite_name} disabled succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:90
#, elixir-autogen, elixir-format
msgid "%{invite_name} enabled succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:68
#, elixir-autogen, elixir-format
msgid "%{invite_name} updated succesfully"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.ex:42
#, elixir-autogen, elixir-format
msgid "%{invite_name} updated successfully"
msgstr ""
#: lib/memex_web/live/invite_live/index.ex:139
#, elixir-autogen, elixir-format
msgid "%{user_email} deleted succesfully"
msgstr ""
#: 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 ""
#: lib/memex_web/live/invite_live/index.ex:127
#, elixir-autogen, elixir-format
msgid "Copied to clipboard"
msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:77
#, elixir-autogen, elixir-format
msgid "Email changed successfully."
msgstr ""
#: 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 ""
#: 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 ""
#: lib/memex_web/controllers/user_settings_controller.ex:65
#, elixir-autogen, elixir-format
msgid "Language updated successfully."
msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:46
#, elixir-autogen, elixir-format
msgid "Password reset successfully."
msgstr ""
#: 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
msgid "Please check your email to verify your account"
msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:30
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
#: 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
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:37
#: lib/memex_web/live/note_live/index.html.heex:46
#: lib/memex_web/live/note_live/show.html.heex:34
#: lib/memex_web/live/pipeline_live/index.html.heex:46
#: lib/memex_web/live/pipeline_live/show.html.heex:46
#: lib/memex_web/live/pipeline_live/show.html.heex:116
#, 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 ""
#: lib/memex_web/controllers/user_session_controller.ex:23
#, elixir-autogen, elixir-format, fuzzy
msgid "logged out successfully."
msgstr ""

View File

@ -10,647 +10,297 @@
msgid "" msgid ""
msgstr "" msgstr ""
#: lib/memex_web/live/invite_live/index.html.heex:90
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:73
msgid "Accessible from any internet-capable device"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:89
msgid "Admins" msgid "Admins"
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_confirmation_controller.ex:8
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:53
msgid "Built with sharing and collaboration in mind"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:78
msgid "Confirm new password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_confirmation_controller.ex:8
msgid "Confirm your account" msgid "Confirm your account"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_session/new.html.heex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:63
msgid "Contexts"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:22
msgid "Contexts:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:70
msgid "Convenient:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:32
#: lib/memex_web/templates/user_settings/edit.html.heex:87
msgid "Current password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:58
msgid "Disable"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:15
msgid "Document notes about individual items or concepts"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:35
msgid "Document your processes, attaching contexts to each step"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:33
msgid "Edit Invite"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:62
msgid "Enable"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:36
#: lib/memex_web/templates/user_settings/edit.html.heex:126
msgid "English"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:44
msgid "Features"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:9
msgid "Forgot your password?"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.ex:12
msgid "Home"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/invite_card.ex:25
msgid "Invite Disabled"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:78
#: lib/memex_web/live/invite_live/index.ex:41
#: lib/memex_web/live/invite_live/index.html.heex:3
msgid "Invites"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_session/new.html.heex:27
msgid "Keep me logged in for 60 days" msgid "Keep me logged in for 60 days"
msgstr "" msgstr ""
#: lib/memex_web/templates/user_registration/new.html.heex:32
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_registration/new.html.heex:32
msgid "Language" msgid "Language"
msgstr "" msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/live.html.heex:37
msgid "Loading..." msgid "Loading..."
msgstr "" msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:20
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_session_controller.ex:8
msgid "Log in"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:50
msgid "Multi-user:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:20
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: lib/memex_web/templates/layout/live.html.heex:50
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.ex:37
msgid "New Invite"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/user_settings/edit.html.heex:71
msgid "New password"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:8
msgid "No invites 😔"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:70
msgid "Notes"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:12
msgid "Notes:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/topbar.ex:56
msgid "Pipelines"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:32
msgid "Pipelines:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:63
msgid "Privacy controls on a per-note, context or pipeline basis"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:60
msgid "Privacy:"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/live/home_live.html.heex:25
msgid "Provide context around a single topic and hotlink to your notes"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/templates/layout/live.html.heex:50
msgid "Reconnecting..." msgid "Reconnecting..."
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_registration_controller.ex:35
msgid "Register"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_reset_password_controller.ex:36
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_settings_controller.ex:10
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:78
msgid "Set Unlimited"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/controllers/user_settings_controller.ex:10
#: lib/memex_web/templates/user_settings/edit.html.heex:3
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
#: lib/memex_web/components/user_card.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/components/user_card.ex:30
msgid "User registered on" msgid "User registered on"
msgstr "" msgstr ""
#: lib/memex_web/components/invite_card.ex:19
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/index.html.heex:118
msgid "Users"
msgstr ""
#, elixir-autogen, elixir-format
#: lib/memex_web/components/invite_card.ex:20
msgid "Uses Left:" msgid "Uses Left:"
msgstr "" msgstr ""
#: lib/memex_web/live/invite_live/form_component.html.heex:24
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
#: lib/memex_web/live/invite_live/form_component.html.heex:24
msgid "Uses left" msgid "Uses left"
msgstr "" msgstr ""
#: lib/memex_web/live/context_live/show.html.heex:17
#: lib/memex_web/live/note_live/show.html.heex:17
#: lib/memex_web/live/pipeline_live/show.html.heex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Visibility: %{visibility}"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:76
#, elixir-autogen, elixir-format
msgid "accessible from any internet-capable device"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:90
#, elixir-autogen, elixir-format
msgid "admins:"
msgstr ""
#: lib/memex_web/live/home_live.html.heex:58
#, elixir-autogen, elixir-format
msgid "built with sharing and collaboration in mind"
msgstr ""
#: lib/memex_web/templates/user_settings/edit.html.heex:78
#, elixir-autogen, elixir-format
msgid "confirm new password"
msgstr ""
#: lib/memex_web/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_registration/new.html.heex:36
#: 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:48
#: lib/memex_web/components/notes_table_component.ex:48
#: 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:49
#: lib/memex_web/components/notes_table_component.ex:49
#: 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:15
#, elixir-autogen, elixir-format
msgid "home"
msgstr ""
#: lib/memex_web/live/context_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "use [[note-slug]] to link to a note"
msgstr ""
#: lib/memex_web/live/faq_live.ex:10
#: lib/memex_web/live/faq_live.html.heex:3
#, elixir-autogen, elixir-format
msgid "faq"
msgstr ""
#: lib/memex_web/components/topbar.ex:23
#: lib/memex_web/live/home_live.html.heex:3 #: lib/memex_web/live/home_live.html.heex:3
#: lib/memex_web/templates/layout/root.html.heex:8
#: lib/memex_web/templates/layout/root.html.heex:9
#: lib/memex_web/views/layout_view.ex:11
#, 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:68
#, 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:62
#, elixir-autogen, elixir-format
msgid "no steps"
msgstr ""
#: lib/memex_web/live/pipeline_live/show.html.heex:57
#, elixir-autogen, elixir-format
msgid "steps:"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:14
#, elixir-autogen, elixir-format
msgid "title"
msgstr ""
#: lib/memex_web/live/step_live/form_component.html.heex:23
#, elixir-autogen, elixir-format
msgid "use [[context-slug]] to link to a context"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:72
#, elixir-autogen, elixir-format
msgid "finally, i wanted to externalize the processes for common situations that use these thought processes at discrete steps. these are pipelines!"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:102
#, elixir-autogen, elixir-format
msgid "for instance, a good context could be what makes some physical designs spark joy for you, and in that context you could backlink to the spoon note as an example of how it fits nicely into your hand."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:118
#, elixir-autogen, elixir-format
msgid "for instance, a pipeline for buying an object could have a step where you consider how much it sparks joy, and it could backlink to the physical designs context, maybe with some notes about how it applies in this case."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:62
#, elixir-autogen, elixir-format
msgid "i really admired the idea of a zettelkasten, especially with org-mode backlinks, however I felt like my notes would immediately become too messy by just putting everything into a single hierarchy."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:67
#, elixir-autogen, elixir-format
msgid "i wanted to separate between a personal dictionary of concepts and then my thought processes that are built off of my experiences and life lessons. these are notes, and contexts, respectively."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:99
#, elixir-autogen, elixir-format
msgid "in my opinion, contexts should be like single-topic blog posts."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:83
#, elixir-autogen, elixir-format
msgid "in my opinion, notes should be written by any of the discrete objects or concepts that are meaningful to you in your life."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:113
#, elixir-autogen, elixir-format
msgid "in my opinion, pipelines should be pretty lightweight, and just backlink to contexts to provide most of the heavy lifting."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:31
#, elixir-autogen, elixir-format
msgid "memex" msgid "memex"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:51
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "org-mode" #: lib/memex_web/live/home_live.html.heex:87
msgid "Admins:"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:20
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "some things that this memex is very loosely inspired by:" #: lib/memex_web/live/note_live/form_component.html.heex:20
msgid "Content"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:88
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "spoons? probably not. a particular brand of spoons that you really like? why not :)" #: lib/memex_web/live/home_live.html.heex:134
msgid "Get involved!"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:14
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "this is a memex, used to document not just your notes, but also your perspectives and processes." #: lib/memex_web/live/home_live.html.heex:151
msgid "Help translate"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:96
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "what should my contexts be like?" #: lib/memex_web/live/home_live.html.heex:82
msgid "Instance Information"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:80
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "what should my notes be like?" #: lib/memex_web/live/home_live.html.heex:113
msgid "Invite Only"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:110
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "what should my pipelines be like?" #: lib/memex_web/live/home_live.html.heex:112
msgid "Public Signups"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:59
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "why split up into notes, contexts and pipelines?" #: lib/memex_web/live/home_live.html.heex:160
msgid "Report bugs or request features"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:41
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "zettelkasten" #: lib/memex_web/live/note_live/form_component.html.heex:35
msgid "Save"
msgstr "" msgstr ""
#: lib/memex_web/views/layout_view.ex:10
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "memEx | %{title}" #: lib/memex_web/live/note_live/form_component.html.heex:36
msgid "Saving..."
msgstr "" msgstr ""
#: lib/memex_web/controllers/user_reset_password_controller.ex:9
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "forgot your password?" #: lib/memex_web/live/note_live/form_component.html.heex:13
msgid "Title"
msgstr "" msgstr ""
#: lib/memex_web/live/faq_live.html.heex:126
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "how many people should i invite?" #: lib/memex_web/live/home_live.html.heex:142
msgstr "" msgid "View the source code"
#: lib/memex_web/live/faq_live.html.heex:134
#, elixir-autogen, elixir-format
msgid "note, context and pipeline slugs must be unique, and you are free to backlink to notes not written by you."
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:139
#, elixir-autogen, elixir-format
msgid "so, i'd recommend inviting anyone you'd like to work on your collective memEx. however, when in doubt, hopefully setting up a new instance is easy enough. if it isn't, then feel free to let me know :)"
msgstr ""
#: lib/memex_web/live/faq_live.html.heex:129
#, elixir-autogen, elixir-format
msgid "while memEx fully supports multiple users, each memEx instance should be treated as a single cohesive and collaborative document."
msgstr "" msgstr ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,56 +0,0 @@
defmodule Memex.Repo.Migrations.FixSearch do
use Ecto.Migration
def up do
reset_search_columns()
end
def down do
# no way to rollback this migration since the previous generated search columns were invalid
reset_search_columns()
end
defp reset_search_columns() do
alter table(:notes), do: remove(:search)
alter table(:contexts), do: remove(:search)
alter table(:pipelines), do: remove(:search)
flush()
execute """
ALTER TABLE notes
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(slug, '')), 'A') ||
setweight(to_tsvector('english', coalesce(immutable_array_to_string(tags, ' '), '')), 'B') ||
setweight(to_tsvector('english', coalesce(content, '')), 'C')
) STORED
"""
execute("CREATE INDEX notes_trgm_idx ON notes USING GIN (search)")
execute """
ALTER TABLE contexts
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(slug, '')), '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)")
execute """
ALTER TABLE pipelines
ADD COLUMN search tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', coalesce(slug, '')), '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
end

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ defmodule Memex.NotesTest do
use Memex.DataCase use Memex.DataCase
import Memex.NotesFixtures import Memex.NotesFixtures
alias Memex.{Notes, Notes.Note} alias Memex.{Notes, Notes.Note}
@moduletag :notes_test
@invalid_attrs %{content: nil, tag: nil, slug: nil, visibility: nil} @invalid_attrs %{content: nil, tag: nil, title: nil, visibility: nil}
describe "notes" do describe "notes" do
setup do setup do
@ -11,44 +11,12 @@ defmodule Memex.NotesTest do
end end
test "list_notes/1 returns all notes for a user", %{user: user} do test "list_notes/1 returns all notes for a user", %{user: user} do
note_a = note_fixture(%{slug: "a", visibility: :public}, user) note_a = note_fixture(%{title: "a", visibility: :public}, user)
note_b = note_fixture(%{slug: "b", visibility: :unlisted}, user) note_b = note_fixture(%{title: "b", visibility: :unlisted}, user)
note_c = note_fixture(%{slug: "c", visibility: :private}, user) note_c = note_fixture(%{title: "c", visibility: :private}, user)
_shouldnt_return = note_fixture(%{visibility: :private}, user_fixture())
assert Notes.list_notes(user) == [note_a, note_b, note_c] assert Notes.list_notes(user) == [note_a, note_b, note_c]
end end
test "list_notes/2 returns relevant notes for a user", %{user: user} do
note_a = note_fixture(%{slug: "dogs", content: "has some treats in it"}, user)
note_b = note_fixture(%{slug: "cats", tags: ["home"]}, user)
note_c =
%{slug: "chickens", content: "bananas stuff", tags: ["life", "decisions"]}
|> note_fixture(user)
_shouldnt_return =
%{slug: "dog", content: "banana treat stuff", visibility: :private}
|> note_fixture(user_fixture())
# slug
assert Notes.list_notes("dog", user) == [note_a]
assert Notes.list_notes("dogs", user) == [note_a]
assert Notes.list_notes("cat", user) == [note_b]
assert Notes.list_notes("chicken", user) == [note_c]
# content
assert Notes.list_notes("treat", user) == [note_a]
assert Notes.list_notes("banana", user) == [note_c]
assert Notes.list_notes("stuff", user) == [note_c]
# tag
assert Notes.list_notes("home", user) == [note_b]
assert Notes.list_notes("life", user) == [note_c]
assert Notes.list_notes("decision", user) == [note_c]
assert Notes.list_notes("decisions", user) == [note_c]
end
test "list_public_notes/0 returns public notes", %{user: user} do test "list_public_notes/0 returns public notes", %{user: user} do
public_note = note_fixture(%{visibility: :public}, user) public_note = note_fixture(%{visibility: :public}, user)
note_fixture(%{visibility: :unlisted}, user) note_fixture(%{visibility: :unlisted}, user)
@ -56,114 +24,23 @@ defmodule Memex.NotesTest do
assert Notes.list_public_notes() == [public_note] assert Notes.list_public_notes() == [public_note]
end end
test "list_public_notes/1 returns relevant notes for a user", %{user: user} do
note_a =
%{slug: "dogs", content: "has some treats in it", visibility: :public}
|> note_fixture(user)
note_b =
%{slug: "cats", tags: ["home"], visibility: :public}
|> note_fixture(user)
note_c =
%{
slug: "chickens",
content: "bananas stuff",
tags: ["life", "decisions"],
visibility: :public
}
|> note_fixture(user)
_shouldnt_return =
%{
slug: "dog",
content: "treats bananas stuff",
tags: ["home", "life", "decisions"],
visibility: :private
}
|> note_fixture(user)
# slug
assert Notes.list_public_notes("dog") == [note_a]
assert Notes.list_public_notes("dogs") == [note_a]
assert Notes.list_public_notes("cat") == [note_b]
assert Notes.list_public_notes("chicken") == [note_c]
# content
assert Notes.list_public_notes("treat") == [note_a]
assert Notes.list_public_notes("banana") == [note_c]
assert Notes.list_public_notes("stuff") == [note_c]
# tag
assert Notes.list_public_notes("home") == [note_b]
assert Notes.list_public_notes("life") == [note_c]
assert Notes.list_public_notes("decision") == [note_c]
assert Notes.list_public_notes("decisions") == [note_c]
end
test "get_note!/1 returns the note with given id", %{user: user} do test "get_note!/1 returns the note with given id", %{user: user} do
note = note_fixture(%{visibility: :public}, user) note = note_fixture(user)
assert Notes.get_note!(note.id, user) == note assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :unlisted}, user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :private}, user)
assert Notes.get_note!(note.id, user) == note
end
test "get_note!/1 only returns unlisted or public notes for other users", %{user: user} do
another_user = user_fixture()
note = note_fixture(%{visibility: :public}, another_user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :unlisted}, another_user)
assert Notes.get_note!(note.id, user) == note
note = note_fixture(%{visibility: :private}, another_user)
assert_raise Ecto.NoResultsError, fn ->
Notes.get_note!(note.id, user)
end
end
test "get_note_by_slug/1 returns the note with given id", %{user: user} do
note = note_fixture(%{slug: "a", visibility: :public}, user)
assert Notes.get_note_by_slug("a", user) == note
note = note_fixture(%{slug: "b", visibility: :unlisted}, user)
assert Notes.get_note_by_slug("b", user) == note
note = note_fixture(%{slug: "c", visibility: :private}, user)
assert Notes.get_note_by_slug("c", user) == note
end
test "get_note_by_slug/1 only returns unlisted or public notes for other users", %{
user: user
} do
another_user = user_fixture()
note = note_fixture(%{slug: "a", visibility: :public}, another_user)
assert Notes.get_note_by_slug("a", user) == note
note = note_fixture(%{slug: "b", visibility: :unlisted}, another_user)
assert Notes.get_note_by_slug("b", user) == note
note_fixture(%{slug: "c", visibility: :private}, another_user)
assert Notes.get_note_by_slug("c", user) |> is_nil()
end end
test "create_note/1 with valid data creates a note", %{user: user} do test "create_note/1 with valid data creates a note", %{user: user} do
valid_attrs = %{ valid_attrs = %{
"content" => "some content", "content" => "some content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"slug" => "some-slug", "title" => "some title",
"visibility" => :public "visibility" => :public
} }
assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user) assert {:ok, %Note{} = note} = Notes.create_note(valid_attrs, user)
assert note.content == "some content" assert note.content == "some content"
assert note.tags == ["tag1", "tag2"] assert note.tags == ["tag1", "tag2"]
assert note.slug == "some-slug" assert note.title == "some title"
assert note.visibility == :public assert note.visibility == :public
end end
@ -177,14 +54,14 @@ defmodule Memex.NotesTest do
update_attrs = %{ update_attrs = %{
"content" => "some updated content", "content" => "some updated content",
"tags_string" => "tag1,tag2", "tags_string" => "tag1,tag2",
"slug" => "some-updated-slug", "title" => "some updated title",
"visibility" => :private "visibility" => :private
} }
assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs, user) assert {:ok, %Note{} = note} = Notes.update_note(note, update_attrs, user)
assert note.content == "some updated content" assert note.content == "some updated content"
assert note.tags == ["tag1", "tag2"] assert note.tags == ["tag1", "tag2"]
assert note.slug == "some-updated-slug" assert note.title == "some updated title"
assert note.visibility == :private assert note.visibility == :private
end end
@ -200,13 +77,6 @@ defmodule Memex.NotesTest do
assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end
end end
test "delete_note/1 deletes the note for an admin user", %{user: user} do
admin_user = admin_fixture()
note = note_fixture(user)
assert {:ok, %Note{}} = Notes.delete_note(note, admin_user)
assert_raise Ecto.NoResultsError, fn -> Notes.get_note!(note.id, user) end
end
test "change_note/1 returns a note changeset", %{user: user} do test "change_note/1 returns a note changeset", %{user: user} do
note = note_fixture(user) note = note_fixture(user)
assert %Ecto.Changeset{} = Notes.change_note(note, user) assert %Ecto.Changeset{} = Notes.change_note(note, user)

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ defmodule MemexWeb.UserResetPasswordControllerTest do
test "renders the reset password page", %{conn: conn} do test "renders the reset password page", %{conn: conn} do
conn = get(conn, Routes.user_reset_password_path(conn, :new)) conn = get(conn, Routes.user_reset_password_path(conn, :new))
response = html_response(conn, 200) response = html_response(conn, 200)
assert response =~ dgettext("actions", "forgot your password?") assert response =~ dgettext("actions", "Forgot your password?")
end end
end end

Some files were not shown because too many files have changed in this diff Show More