Compare commits
54 commits
1809f945c6
...
f78af227ff
| Author | SHA1 | Date | |
|---|---|---|---|
| f78af227ff | |||
| 1858542f07 | |||
| 0c48ed4320 | |||
| 8507109631 | |||
| 6907b2ed3c | |||
| 4ee629366b | |||
| 99f5ba8a2b | |||
|
|
9c254d54f2 | ||
| 68560ad46b | |||
|
|
cc54e86667 | ||
| 3d2cbb6a15 | |||
|
|
f2a19ca8fb | ||
| 25473afc43 | |||
| 06574a932d | |||
| 33d4fa66c8 | |||
| 7a75f8f526 | |||
| a09e2add2f | |||
| 662e80cc74 | |||
| 2e256a0206 | |||
| 5959c9f545 | |||
| df9966bb12 | |||
| fd8c853879 | |||
| 681db5dc71 | |||
| 5287a20b98 | |||
|
|
96434c020b | ||
|
|
bba7683200 | ||
|
|
fd8f046298 | ||
| 18f0b44144 | |||
|
|
8b86b7139c | ||
| 5c4f8ec07e | |||
| ed9616035b | |||
| ab81b29467 | |||
| 50c80eed38 | |||
| f17f8fe74d | |||
| bbf760c2b5 | |||
| f485f7bd8f | |||
|
|
d620e9077a | ||
| 7aa53dc9ef | |||
| aa843933f9 | |||
| 4dd114c22a | |||
| 2255dfbf6e | |||
| c2cb75a32b | |||
| acaa12fea6 | |||
| 15d6fd38c9 | |||
| afda276d22 | |||
| 0334260de5 | |||
| 50832da885 | |||
| d89b1d1cc0 | |||
| c840749837 | |||
|
|
d4dd386283 | ||
| 15316c7f3b | |||
| 205e360463 | |||
|
|
51911e3fb4 | ||
|
|
f307294849 |
63 changed files with 5306 additions and 1578 deletions
|
|
@ -118,7 +118,7 @@ environment:
|
|||
|
||||
steps:
|
||||
- name: renovate
|
||||
image: renovate/renovate:40.62
|
||||
image: renovate/renovate:41.46
|
||||
environment:
|
||||
RENOVATE_CONFIG_FILE: "renovate_backend_config.js"
|
||||
RENOVATE_TOKEN:
|
||||
|
|
|
|||
1
.env.example
Normal file
1
.env.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
OIDC_CLIENT_SECRET=
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -39,3 +39,5 @@ npm-debug.log
|
|||
|
||||
# Ignore the .env file with env variables
|
||||
.env
|
||||
|
||||
.elixir_ls/
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
elixir 1.18.3-otp-27
|
||||
erlang 27.3.4
|
||||
just 1.40.0
|
||||
just 1.42.4
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -1,2 +1,18 @@
|
|||
# mitgliederverwaltung
|
||||
|
||||
## Testing SSO with rauthy
|
||||
|
||||
1. `just run`
|
||||
1. go to [localhost:8080], go to the Admin area
|
||||
1. Login with "admin@localhost" and password from `dev/rauthy/config.toml`
|
||||
1. add client from the admin panel
|
||||
- Client ID: mv
|
||||
- redirect uris: http://localhost:4000/auth/user/rauthy/callback
|
||||
- Authorization Flows: authorization_code
|
||||
- allowed origins: http://localhost:4000
|
||||
- access/id token algortihm: RS256 (EDDSA did not work for me, found just few infos in the ashauthentication docs)
|
||||
1. copy client secret to `.env` file
|
||||
1. abort and run `just run` again
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,102 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
/* See the Tailwind configuration guide for advanced usage
|
||||
https://tailwindcss.com/docs/configuration */
|
||||
|
||||
@import "tailwindcss" source(none);
|
||||
@source "../css";
|
||||
@source "../js";
|
||||
@source "../../lib/mv_web";
|
||||
|
||||
/* A Tailwind plugin that makes "hero-#{ICON}" classes available.
|
||||
The heroicons installation itself is managed by your mix.exs */
|
||||
@plugin "../vendor/heroicons";
|
||||
|
||||
/* daisyUI Tailwind Plugin. You can update this file by fetching the latest version with:
|
||||
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
|
||||
Make sure to look at the daisyUI changelog: https://daisyui.com/docs/changelog/ */
|
||||
@plugin "../vendor/daisyui" {
|
||||
themes: false;
|
||||
}
|
||||
|
||||
/* daisyUI theme plugin. You can update this file by fetching the latest version with:
|
||||
curl -sLO https://github.com/saadeghi/daisyui/releases/latest/download/daisyui-theme.js
|
||||
We ship with two themes, a light one inspired on Phoenix colors and a dark one inspired
|
||||
on Elixir colors. Build your own at: https://daisyui.com/theme-generator/ */
|
||||
@plugin "../vendor/daisyui-theme" {
|
||||
name: "dark";
|
||||
default: false;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(30.33% 0.016 252.42);
|
||||
--color-base-200: oklch(25.26% 0.014 253.1);
|
||||
--color-base-300: oklch(20.15% 0.012 254.09);
|
||||
--color-base-content: oklch(97.807% 0.029 256.847);
|
||||
--color-primary: oklch(58% 0.233 277.117);
|
||||
--color-primary-content: oklch(96% 0.018 272.314);
|
||||
--color-secondary: oklch(58% 0.233 277.117);
|
||||
--color-secondary-content: oklch(96% 0.018 272.314);
|
||||
--color-accent: oklch(60% 0.25 292.717);
|
||||
--color-accent-content: oklch(96% 0.016 293.756);
|
||||
--color-neutral: oklch(37% 0.044 257.287);
|
||||
--color-neutral-content: oklch(98% 0.003 247.858);
|
||||
--color-info: oklch(58% 0.158 241.966);
|
||||
--color-info-content: oklch(97% 0.013 236.62);
|
||||
--color-success: oklch(60% 0.118 184.704);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@plugin "../vendor/daisyui-theme" {
|
||||
name: "light";
|
||||
default: true;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(98% 0 0);
|
||||
--color-base-200: oklch(96% 0.001 286.375);
|
||||
--color-base-300: oklch(92% 0.004 286.32);
|
||||
--color-base-content: oklch(21% 0.006 285.885);
|
||||
--color-primary: oklch(70% 0.213 47.604);
|
||||
--color-primary-content: oklch(98% 0.016 73.684);
|
||||
--color-secondary: oklch(55% 0.027 264.364);
|
||||
--color-secondary-content: oklch(98% 0.002 247.839);
|
||||
--color-accent: oklch(0% 0 0);
|
||||
--color-accent-content: oklch(100% 0 0);
|
||||
--color-neutral: oklch(44% 0.017 285.786);
|
||||
--color-neutral-content: oklch(98% 0 0);
|
||||
--color-info: oklch(62% 0.214 259.815);
|
||||
--color-info-content: oklch(97% 0.014 254.604);
|
||||
--color-success: oklch(70% 0.14 182.503);
|
||||
--color-success-content: oklch(98% 0.014 180.72);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.25rem;
|
||||
--radius-box: 0.5rem;
|
||||
--size-selector: 0.21875rem;
|
||||
--size-field: 0.21875rem;
|
||||
--border: 1.5px;
|
||||
--depth: 1;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
/* Add variants based on LiveView classes */
|
||||
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
|
||||
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
|
||||
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
|
||||
|
||||
/* Make LiveView wrapper divs transparent for layout */
|
||||
[data-phx-session] { display: contents }
|
||||
|
||||
/* This file is for your main application CSS */
|
||||
|
|
|
|||
124
assets/vendor/daisyui-theme.js
vendored
Normal file
124
assets/vendor/daisyui-theme.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1021
assets/vendor/daisyui.js
vendored
Normal file
1021
assets/vendor/daisyui.js
vendored
Normal file
File diff suppressed because one or more lines are too long
43
assets/vendor/heroicons.js
vendored
Normal file
43
assets/vendor/heroicons.js
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
const plugin = require("tailwindcss/plugin")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
module.exports = plugin(function({matchComponents, theme}) {
|
||||
let iconsDir = path.join(__dirname, "../../deps/heroicons/optimized")
|
||||
let values = {}
|
||||
let icons = [
|
||||
["", "/24/outline"],
|
||||
["-solid", "/24/solid"],
|
||||
["-mini", "/20/solid"],
|
||||
["-micro", "/16/solid"]
|
||||
]
|
||||
icons.forEach(([suffix, dir]) => {
|
||||
fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
|
||||
let name = path.basename(file, ".svg") + suffix
|
||||
values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
|
||||
})
|
||||
})
|
||||
matchComponents({
|
||||
"hero": ({name, fullPath}) => {
|
||||
let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
|
||||
content = encodeURIComponent(content)
|
||||
let size = theme("spacing.6")
|
||||
if (name.endsWith("-mini")) {
|
||||
size = theme("spacing.5")
|
||||
} else if (name.endsWith("-micro")) {
|
||||
size = theme("spacing.4")
|
||||
}
|
||||
return {
|
||||
[`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
|
||||
"-webkit-mask": `var(--hero-${name})`,
|
||||
"mask": `var(--hero-${name})`,
|
||||
"mask-repeat": "no-repeat",
|
||||
"background-color": "currentColor",
|
||||
"vertical-align": "middle",
|
||||
"display": "inline-block",
|
||||
"width": size,
|
||||
"height": size
|
||||
}
|
||||
}
|
||||
}, {values})
|
||||
})
|
||||
37
assets/vendor/topbar.js
vendored
37
assets/vendor/topbar.js
vendored
|
|
@ -1,39 +1,12 @@
|
|||
/**
|
||||
* @license MIT
|
||||
* topbar 2.0.0, 2023-02-04
|
||||
* https://buunguyen.github.io/topbar
|
||||
* Copyright (c) 2021 Buu Nguyen
|
||||
* topbar 3.0.0
|
||||
* http://buunguyen.github.io/topbar
|
||||
* Copyright (c) 2024 Buu Nguyen
|
||||
*/
|
||||
(function (window, document) {
|
||||
"use strict";
|
||||
|
||||
// https://gist.github.com/paulirish/1579671
|
||||
(function () {
|
||||
var lastTime = 0;
|
||||
var vendors = ["ms", "moz", "webkit", "o"];
|
||||
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
|
||||
window.requestAnimationFrame =
|
||||
window[vendors[x] + "RequestAnimationFrame"];
|
||||
window.cancelAnimationFrame =
|
||||
window[vendors[x] + "CancelAnimationFrame"] ||
|
||||
window[vendors[x] + "CancelRequestAnimationFrame"];
|
||||
}
|
||||
if (!window.requestAnimationFrame)
|
||||
window.requestAnimationFrame = function (callback, element) {
|
||||
var currTime = new Date().getTime();
|
||||
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
||||
var id = window.setTimeout(function () {
|
||||
callback(currTime + timeToCall);
|
||||
}, timeToCall);
|
||||
lastTime = currTime + timeToCall;
|
||||
return id;
|
||||
};
|
||||
if (!window.cancelAnimationFrame)
|
||||
window.cancelAnimationFrame = function (id) {
|
||||
clearTimeout(id);
|
||||
};
|
||||
})();
|
||||
|
||||
var canvas,
|
||||
currentProgress,
|
||||
showing,
|
||||
|
|
@ -88,7 +61,6 @@
|
|||
style.zIndex = 100001;
|
||||
style.display = "none";
|
||||
if (options.className) canvas.classList.add(options.className);
|
||||
document.body.appendChild(canvas);
|
||||
addEvent(window, "resize", repaint);
|
||||
},
|
||||
topbar = {
|
||||
|
|
@ -101,10 +73,11 @@
|
|||
if (delay) {
|
||||
if (delayTimerId) return;
|
||||
delayTimerId = setTimeout(() => topbar.show(), delay);
|
||||
} else {
|
||||
} else {
|
||||
showing = true;
|
||||
if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
|
||||
if (!canvas) createCanvas();
|
||||
if (!canvas.parentElement) document.body.appendChild(canvas);
|
||||
canvas.style.opacity = 1;
|
||||
canvas.style.display = "block";
|
||||
topbar.progress(0);
|
||||
|
|
|
|||
|
|
@ -76,25 +76,24 @@ config :esbuild,
|
|||
version: "0.17.11",
|
||||
mv: [
|
||||
args:
|
||||
~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
|
||||
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
|
||||
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
|
||||
]
|
||||
|
||||
# Configure tailwind (the version is required)
|
||||
config :tailwind,
|
||||
version: "3.4.3",
|
||||
version: "4.0.9",
|
||||
mv: [
|
||||
args: ~w(
|
||||
--config=tailwind.config.js
|
||||
--input=css/app.css
|
||||
--output=../priv/static/assets/app.css
|
||||
--input=assets/css/app.css
|
||||
--output=priv/static/assets/css/app.css
|
||||
),
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
cd: Path.expand("..", __DIR__)
|
||||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
config :logger, :default_formatter,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
metadata: [:request_id]
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@ config :mv, Mv.Repo,
|
|||
# The watchers configuration can be used to run external
|
||||
# watchers to your application. For example, we can use it
|
||||
# to bundle .js and .css sources.
|
||||
# Binding to loopback ipv4 address prevents access from other machines.
|
||||
config :mv, MvWeb.Endpoint,
|
||||
# Binding to loopback ipv4 address prevents access from other machines.
|
||||
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
|
||||
http: [ip: {127, 0, 0, 1}, port: 4000],
|
||||
http: [ip: {127, 0, 0, 1}, port: String.to_integer(System.get_env("PORT") || "4000")],
|
||||
check_origin: false,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
|
|
@ -56,10 +56,11 @@ config :mv, MvWeb.Endpoint,
|
|||
# Watch static and templates for browser reloading.
|
||||
config :mv, MvWeb.Endpoint,
|
||||
live_reload: [
|
||||
web_console_logger: true,
|
||||
patterns: [
|
||||
~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
|
||||
~r"priv/gettext/.*(po)$",
|
||||
~r"lib/mv_web/(controllers|live|components)/.*(ex|heex)$"
|
||||
~r"lib/mv_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$"
|
||||
]
|
||||
]
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ config :mv, MvWeb.Endpoint,
|
|||
config :mv, dev_routes: true
|
||||
|
||||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
config :logger, :default_formatter, format: "[$level] $message\n"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
|
|
@ -77,8 +78,10 @@ config :phoenix, :stacktrace_depth, 20
|
|||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :phoenix_live_view,
|
||||
# Include HEEx debug annotations as HTML comments in rendered markup
|
||||
# Include HEEx debug annotations as HTML comments in rendered markup.
|
||||
# Changing this configuration will require mix clean and a full recompile.
|
||||
debug_heex_annotations: true,
|
||||
debug_tags_location: true,
|
||||
# Enable helpful, but potentially expensive runtime checks
|
||||
enable_expensive_runtime_checks: true
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Config
|
|||
config :mv, MvWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
|
||||
# Configures Swoosh API Client
|
||||
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Mv.Finch
|
||||
config :swoosh, api_client: Swoosh.ApiClient.Req
|
||||
|
||||
# Disable Swoosh Local Memory Storage
|
||||
config :swoosh, local: false
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ if config_env() == :prod do
|
|||
# domain: System.get_env("MAILGUN_DOMAIN")
|
||||
#
|
||||
# For this example you need include a HTTP client required by Swoosh API client.
|
||||
# Swoosh supports Hackney and Finch out of the box:
|
||||
# Swoosh supports Hackney, Req and Finch out of the box:
|
||||
#
|
||||
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
version: "3.5"
|
||||
|
||||
networks:
|
||||
local:
|
||||
rauthy-dev:
|
||||
|
|
@ -30,10 +28,9 @@ services:
|
|||
networks:
|
||||
- rauthy-dev
|
||||
|
||||
|
||||
rauthy:
|
||||
container_name: rauthy-dev
|
||||
image: ghcr.io/sebadob/rauthy:0.31.2
|
||||
image: ghcr.io/sebadob/rauthy:0.31.3
|
||||
environment:
|
||||
- LOCAL_TEST=true
|
||||
- SMTP_URL=mailcrab
|
||||
|
|
@ -56,13 +53,11 @@ services:
|
|||
networks:
|
||||
- rauthy-dev
|
||||
- local
|
||||
volumes:
|
||||
- type: volume
|
||||
source: rauthy-data
|
||||
volumes:
|
||||
- type: volume
|
||||
source: rauthy-data
|
||||
target: /app/data
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
rauthy-data:
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,20 @@ defmodule Mv.Accounts do
|
|||
AshAuthentication specific domain to handle Authentication for users.
|
||||
"""
|
||||
use Ash.Domain,
|
||||
extensions: [AshPhoenix]
|
||||
extensions: [AshAdmin.Domain, AshPhoenix]
|
||||
|
||||
admin do
|
||||
show? true
|
||||
end
|
||||
|
||||
resources do
|
||||
resource Mv.Accounts.User do
|
||||
define :create_user, action: :create
|
||||
define :create_user, action: :create_user
|
||||
define :list_users, action: :read
|
||||
define :update_user, action: :update
|
||||
define :update_user, action: :update_user
|
||||
define :destroy_user, action: :destroy
|
||||
define :create_register_with_rauthy, action: :register_with_rauthy
|
||||
define :read_sign_in_with_rauthy, action: :sign_in_with_rauthy
|
||||
end
|
||||
|
||||
resource Mv.Accounts.Token
|
||||
|
|
|
|||
|
|
@ -63,6 +63,27 @@ defmodule Mv.Accounts.User do
|
|||
actions do
|
||||
defaults [:read, :create, :destroy, :update]
|
||||
|
||||
create :create_user do
|
||||
accept [:email]
|
||||
end
|
||||
|
||||
update :update_user do
|
||||
accept [:email]
|
||||
end
|
||||
|
||||
# Admin action for direct password changes in admin panel
|
||||
# Uses the official Ash Authentication HashPasswordChange with correct context
|
||||
update :admin_set_password do
|
||||
accept [:email]
|
||||
argument :password, :string, allow_nil?: false, sensitive?: true
|
||||
|
||||
# Set the strategy context that HashPasswordChange expects
|
||||
change set_context(%{strategy_name: :password})
|
||||
|
||||
# Use the official Ash Authentication password change
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
end
|
||||
|
||||
read :get_by_subject do
|
||||
description "Get a user by the subject claim in a JWT"
|
||||
argument :subject, :string, allow_nil?: false
|
||||
|
|
@ -75,14 +96,16 @@ defmodule Mv.Accounts.User do
|
|||
argument :oauth_tokens, :map, allow_nil?: false
|
||||
prepare AshAuthentication.Strategy.OAuth2.SignInPreparation
|
||||
|
||||
filter expr(email == get_path(^arg(:user_info), [:email]))
|
||||
filter expr(email == get_path(^arg(:user_info), [:preferred_username]))
|
||||
end
|
||||
|
||||
create :register_with_rauthy do
|
||||
argument :user_info, :map, allow_nil?: false
|
||||
argument :oauth_tokens, :map, allow_nil?: false
|
||||
upsert? true
|
||||
upsert_identity :unique_email
|
||||
upsert_identity :unique_oidc_id
|
||||
|
||||
validate &__MODULE__.validate_oidc_id_present/2
|
||||
|
||||
change AshAuthentication.GenerateTokenChange
|
||||
|
||||
|
|
@ -91,11 +114,29 @@ defmodule Mv.Accounts.User do
|
|||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:email, user_info["preferred_username"])
|
||||
|> Ash.Changeset.change_attribute(:oidc_id, user_info["id"])
|
||||
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Global validations - applied to all relevant actions
|
||||
validations do
|
||||
# Password strength policy: minimum 8 characters for all password-related actions
|
||||
validate string_length(:password, min: 8) do
|
||||
where action_is([:register_with_password, :admin_set_password])
|
||||
end
|
||||
end
|
||||
|
||||
def validate_oidc_id_present(changeset, _context) do
|
||||
user_info = Ash.Changeset.get_argument(changeset, :user_info) || %{}
|
||||
|
||||
if is_binary(user_info["sub"]) or is_binary(user_info["id"]) do
|
||||
:ok
|
||||
else
|
||||
{:error, [user_info: "OIDC user_info must contain a non-empty 'sub' or 'id' field"]}
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
defmodule Mv.Membership do
|
||||
use Ash.Domain,
|
||||
extensions: [AshPhoenix]
|
||||
extensions: [AshAdmin.Domain, AshPhoenix]
|
||||
|
||||
admin do
|
||||
show? true
|
||||
end
|
||||
|
||||
resources do
|
||||
resource Mv.Membership.Member do
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ defmodule Mv.Application do
|
|||
Mv.Repo,
|
||||
{DNSCluster, query: Application.get_env(:mv, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Mv.PubSub},
|
||||
# Start the Finch HTTP client for sending emails
|
||||
{Finch, name: Mv.Finch},
|
||||
{AshAuthentication.Supervisor, otp_app: :my},
|
||||
# Start a worker by calling: Mv.Worker.start_link(arg)
|
||||
# {Mv.Worker, arg},
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ defmodule MvWeb do
|
|||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller,
|
||||
formats: [:html, :json],
|
||||
layouts: [html: MvWeb.Layouts]
|
||||
use Phoenix.Controller, formats: [:html, :json]
|
||||
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
|
|
@ -52,10 +50,10 @@ defmodule MvWeb do
|
|||
|
||||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView,
|
||||
layout: {MvWeb.Layouts, :app}
|
||||
use Phoenix.LiveView
|
||||
|
||||
on_mount MvWeb.LiveHelpers
|
||||
|
||||
unquote(html_helpers())
|
||||
end
|
||||
end
|
||||
|
|
@ -91,8 +89,9 @@ defmodule MvWeb do
|
|||
# Core UI components
|
||||
import MvWeb.CoreComponents
|
||||
|
||||
# Shortcut for generating JS commands
|
||||
# Common modules used in templates
|
||||
alias Phoenix.LiveView.JS
|
||||
alias MvWeb.Layouts
|
||||
|
||||
# Routes generation with the ~p sigil
|
||||
unquote(verified_routes())
|
||||
|
|
|
|||
|
|
@ -3,92 +3,34 @@ defmodule MvWeb.CoreComponents do
|
|||
Provides core UI components.
|
||||
|
||||
At first glance, this module may seem daunting, but its goal is to provide
|
||||
core building blocks for your application, such as modals, tables, and
|
||||
forms. The components consist mostly of markup and are well-documented
|
||||
core building blocks for your application, such as tables, forms, and
|
||||
inputs. The components consist mostly of markup and are well-documented
|
||||
with doc strings and declarative assigns. You may customize and style
|
||||
them in any way you want, based on your application growth and needs.
|
||||
|
||||
The default components use Tailwind CSS, a utility-first CSS framework.
|
||||
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
|
||||
how to customize them or feel free to swap in another framework altogether.
|
||||
The foundation for styling is Tailwind CSS, a utility-first CSS framework,
|
||||
augmented with daisyUI, a Tailwind CSS plugin that provides UI components
|
||||
and themes. Here are useful references:
|
||||
|
||||
* [daisyUI](https://daisyui.com/docs/intro/) - a good place to get
|
||||
started and see the available components.
|
||||
|
||||
* [Tailwind CSS](https://tailwindcss.com) - the foundational framework
|
||||
we build on. You will use it for layout, sizing, flexbox, grid, and
|
||||
spacing.
|
||||
|
||||
* [Heroicons](https://heroicons.com) - see `icon/1` for usage.
|
||||
|
||||
* [Phoenix.Component](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) -
|
||||
the component system used by Phoenix. Some components, such as `<.link>`
|
||||
and `<.form>`, are defined there.
|
||||
|
||||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
|
||||
@doc """
|
||||
Renders a modal.
|
||||
|
||||
## Examples
|
||||
|
||||
<.modal id="confirm-modal">
|
||||
This is a modal.
|
||||
</.modal>
|
||||
|
||||
JS commands may be passed to the `:on_cancel` to configure
|
||||
the closing/cancel event, for example:
|
||||
|
||||
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
|
||||
This is another modal.
|
||||
</.modal>
|
||||
|
||||
"""
|
||||
attr :id, :string, required: true
|
||||
attr :show, :boolean, default: false
|
||||
attr :on_cancel, JS, default: %JS{}
|
||||
slot :inner_block, required: true
|
||||
|
||||
def modal(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={@id}
|
||||
phx-mounted={@show && show_modal(@id)}
|
||||
phx-remove={hide_modal(@id)}
|
||||
data-cancel={JS.exec(@on_cancel, "phx-remove")}
|
||||
class="relative z-50 hidden"
|
||||
>
|
||||
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
|
||||
<div
|
||||
class="fixed inset-0 overflow-y-auto"
|
||||
aria-labelledby={"#{@id}-title"}
|
||||
aria-describedby={"#{@id}-description"}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex min-h-full items-center justify-center">
|
||||
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
|
||||
<.focus_wrap
|
||||
id={"#{@id}-container"}
|
||||
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
|
||||
phx-key="escape"
|
||||
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
|
||||
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
|
||||
>
|
||||
<div class="absolute top-6 right-5">
|
||||
<button
|
||||
phx-click={JS.exec("data-cancel", to: "##{@id}")}
|
||||
type="button"
|
||||
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
|
||||
aria-label={gettext("close")}
|
||||
>
|
||||
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div id={"#{@id}-content"}>
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</.focus_wrap>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders flash notices.
|
||||
|
||||
|
|
@ -114,132 +56,59 @@ defmodule MvWeb.CoreComponents do
|
|||
id={@id}
|
||||
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
||||
role="alert"
|
||||
class={[
|
||||
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
|
||||
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
|
||||
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
|
||||
]}
|
||||
class="toast toast-top toast-end z-50"
|
||||
{@rest}
|
||||
>
|
||||
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
||||
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
|
||||
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
|
||||
{@title}
|
||||
</p>
|
||||
<p class="mt-2 text-sm leading-5">{msg}</p>
|
||||
<button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
|
||||
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows the flash group with standard titles and content.
|
||||
|
||||
## Examples
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
||||
|
||||
def flash_group(assigns) do
|
||||
~H"""
|
||||
<div id={@id}>
|
||||
<.flash kind={:info} title={gettext("Success!")} flash={@flash} />
|
||||
<.flash kind={:error} title={gettext("Error!")} flash={@flash} />
|
||||
<.flash
|
||||
id="client-error"
|
||||
kind={:error}
|
||||
title={gettext("We can't find the internet")}
|
||||
phx-disconnected={show(".phx-client-error #client-error")}
|
||||
phx-connected={hide("#client-error")}
|
||||
hidden
|
||||
>
|
||||
{gettext("Attempting to reconnect")}
|
||||
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||||
</.flash>
|
||||
|
||||
<.flash
|
||||
id="server-error"
|
||||
kind={:error}
|
||||
title={gettext("Something went wrong!")}
|
||||
phx-disconnected={show(".phx-server-error #server-error")}
|
||||
phx-connected={hide("#server-error")}
|
||||
hidden
|
||||
>
|
||||
{gettext("Hang in there while we get back on track")}
|
||||
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
||||
</.flash>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a simple form.
|
||||
|
||||
## Examples
|
||||
|
||||
<.simple_form for={@form} phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:email]} label="Email"/>
|
||||
<.input field={@form[:username]} label="Username" />
|
||||
<:actions>
|
||||
<.button>Save</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
attr :for, :any, required: true, doc: "the data structure for the form"
|
||||
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
|
||||
doc: "the arbitrary HTML attributes to apply to the form tag"
|
||||
|
||||
slot :inner_block, required: true
|
||||
slot :actions, doc: "the slot for form actions, such as a submit button"
|
||||
|
||||
def simple_form(assigns) do
|
||||
~H"""
|
||||
<.form :let={f} for={@for} as={@as} {@rest}>
|
||||
<div class="mt-10 space-y-8 bg-white">
|
||||
{render_slot(@inner_block, f)}
|
||||
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
|
||||
{render_slot(action, f)}
|
||||
<div class={[
|
||||
"alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
|
||||
@kind == :info && "alert-info",
|
||||
@kind == :error && "alert-error"
|
||||
]}>
|
||||
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
|
||||
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
|
||||
<div>
|
||||
<p :if={@title} class="font-semibold">{@title}</p>
|
||||
<p>{msg}</p>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
|
||||
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
|
||||
</button>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a button.
|
||||
Renders a button with navigation support.
|
||||
|
||||
## Examples
|
||||
|
||||
<.button>Send!</.button>
|
||||
<.button phx-click="go" class="ml-2">Send!</.button>
|
||||
<.button phx-click="go" variant="primary">Send!</.button>
|
||||
<.button navigate={~p"/"}>Home</.button>
|
||||
"""
|
||||
attr :type, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
attr :rest, :global, include: ~w(disabled form name value)
|
||||
|
||||
attr :rest, :global, include: ~w(href navigate patch method)
|
||||
attr :variant, :string, values: ~w(primary)
|
||||
slot :inner_block, required: true
|
||||
|
||||
def button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type={@type}
|
||||
class={[
|
||||
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
|
||||
"text-sm font-semibold leading-6 text-white active:text-white/80",
|
||||
@class
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
</button>
|
||||
"""
|
||||
def button(%{rest: rest} = assigns) do
|
||||
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
||||
assigns = assign(assigns, :class, Map.fetch!(variants, assigns[:variant]))
|
||||
|
||||
if rest[:href] || rest[:navigate] || rest[:patch] do
|
||||
~H"""
|
||||
<.link class={["btn", @class]} {@rest}>
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
"""
|
||||
else
|
||||
~H"""
|
||||
<button class={["btn", @class]} {@rest}>
|
||||
{render_slot(@inner_block)}
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -276,7 +145,7 @@ defmodule MvWeb.CoreComponents do
|
|||
attr :type, :string,
|
||||
default: "text",
|
||||
values: ~w(checkbox color date datetime-local email file month number password
|
||||
range search select tel text textarea time url week)
|
||||
search select tel text textarea time url week)
|
||||
|
||||
attr :field, Phoenix.HTML.FormField,
|
||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||
|
|
@ -286,6 +155,8 @@ defmodule MvWeb.CoreComponents do
|
|||
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
|
||||
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
|
||||
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
|
||||
attr :class, :string, default: nil, doc: "the input class to use over defaults"
|
||||
attr :error_class, :string, default: nil, doc: "the input error class to use over defaults"
|
||||
|
||||
attr :rest, :global,
|
||||
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
||||
|
|
@ -309,108 +180,95 @@ defmodule MvWeb.CoreComponents do
|
|||
end)
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
<label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
|
||||
<fieldset class="fieldset mb-2">
|
||||
<label>
|
||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
|
||||
{@rest}
|
||||
/>
|
||||
{@label}
|
||||
<span class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={@id}
|
||||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class={@class || "checkbox checkbox-sm"}
|
||||
{@rest}
|
||||
/>{@label}
|
||||
</span>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "select"} = assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.label for={@id}>{@label}</.label>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
<option :if={@prompt} value="">{@prompt}</option>
|
||||
{Phoenix.HTML.Form.options_for_select(@options, @value)}
|
||||
</select>
|
||||
<fieldset class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<select
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[@class || "w-full select", @errors != [] && (@error_class || "select-error")]}
|
||||
multiple={@multiple}
|
||||
{@rest}
|
||||
>
|
||||
<option :if={@prompt} value="">{@prompt}</option>
|
||||
{Phoenix.HTML.Form.options_for_select(@options, @value)}
|
||||
</select>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
def input(%{type: "textarea"} = assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.label for={@id}>{@label}</.label>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
|
||||
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
|
||||
<fieldset class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<textarea
|
||||
id={@id}
|
||||
name={@name}
|
||||
class={[
|
||||
@class || "w-full textarea",
|
||||
@errors != [] && (@error_class || "textarea-error")
|
||||
]}
|
||||
{@rest}
|
||||
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
# All other inputs text, datetime-local, url, password, etc. are handled here...
|
||||
def input(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.label for={@id}>{@label}</.label>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
|
||||
@errors == [] && "border-zinc-300 focus:border-zinc-400",
|
||||
@errors != [] && "border-rose-400 focus:border-rose-400"
|
||||
]}
|
||||
{@rest}
|
||||
/>
|
||||
<fieldset class="fieldset mb-2">
|
||||
<label>
|
||||
<span :if={@label} class="label mb-1">{@label}</span>
|
||||
<input
|
||||
type={@type}
|
||||
name={@name}
|
||||
id={@id}
|
||||
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
||||
class={[
|
||||
@class || "w-full input",
|
||||
@errors != [] && (@error_class || "input-error")
|
||||
]}
|
||||
{@rest}
|
||||
/>
|
||||
</label>
|
||||
<.error :for={msg <- @errors}>{msg}</.error>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a label.
|
||||
"""
|
||||
attr :for, :string, default: nil
|
||||
slot :inner_block, required: true
|
||||
|
||||
def label(assigns) do
|
||||
# Helper used by inputs to generate form errors
|
||||
defp error(assigns) do
|
||||
~H"""
|
||||
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
|
||||
{render_slot(@inner_block)}
|
||||
</label>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a generic error message.
|
||||
"""
|
||||
slot :inner_block, required: true
|
||||
|
||||
def error(assigns) do
|
||||
~H"""
|
||||
<p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
|
||||
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
|
||||
<p class="mt-1.5 flex gap-2 items-center text-sm text-error">
|
||||
<.icon name="hero-exclamation-circle" class="size-5" />
|
||||
{render_slot(@inner_block)}
|
||||
</p>
|
||||
"""
|
||||
|
|
@ -427,12 +285,12 @@ defmodule MvWeb.CoreComponents do
|
|||
|
||||
def header(assigns) do
|
||||
~H"""
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
|
||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4", @class]}>
|
||||
<div>
|
||||
<h1 class="text-lg font-semibold leading-8 text-zinc-800">
|
||||
<h1 class="text-lg font-semibold leading-8">
|
||||
{render_slot(@inner_block)}
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
|
||||
<p :if={@subtitle != []} class="text-sm text-base-content/70">
|
||||
{render_slot(@subtitle)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -473,49 +331,34 @@ defmodule MvWeb.CoreComponents do
|
|||
end
|
||||
|
||||
~H"""
|
||||
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
|
||||
<table class="w-[40rem] mt-11 sm:w-full">
|
||||
<thead class="text-sm text-left leading-6 text-zinc-500">
|
||||
<tr>
|
||||
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
|
||||
<th :if={@action != []} class="relative p-0 pb-4">
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
id={@id}
|
||||
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
|
||||
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
|
||||
>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
|
||||
<td
|
||||
:for={{col, i} <- Enum.with_index(@col)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
|
||||
>
|
||||
<div class="block py-4 pr-6">
|
||||
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
|
||||
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td :if={@action != []} class="relative w-14 p-0">
|
||||
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
|
||||
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
|
||||
<span
|
||||
:for={action <- @action}
|
||||
class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th :for={col <- @col}>{col[:label]}</th>
|
||||
<th :if={@action != []}>
|
||||
<span class="sr-only">{gettext("Actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id={@id} phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}>
|
||||
<tr :for={row <- @rows} id={@row_id && @row_id.(row)}>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={@row_click && "hover:cursor-pointer"}
|
||||
>
|
||||
{render_slot(col, @row_item.(row))}
|
||||
</td>
|
||||
<td :if={@action != []} class="w-0 font-semibold">
|
||||
<div class="flex gap-4">
|
||||
<%= for action <- @action do %>
|
||||
{render_slot(action, @row_item.(row))}
|
||||
<% end %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -535,38 +378,14 @@ defmodule MvWeb.CoreComponents do
|
|||
|
||||
def list(assigns) do
|
||||
~H"""
|
||||
<div class="mt-14">
|
||||
<dl class="-my-4 divide-y divide-zinc-100">
|
||||
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
||||
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
|
||||
<dd class="text-zinc-700">{render_slot(item)}</dd>
|
||||
<ul class="list">
|
||||
<li :for={item <- @item} class="list-row">
|
||||
<div>
|
||||
<div class="font-bold">{item.title}</div>
|
||||
<div>{render_slot(item)}</div>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a back navigation link.
|
||||
|
||||
## Examples
|
||||
|
||||
<.back navigate={~p"/posts"}>Back to posts</.back>
|
||||
"""
|
||||
attr :navigate, :any, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
def back(assigns) do
|
||||
~H"""
|
||||
<div class="mt-16">
|
||||
<.link
|
||||
navigate={@navigate}
|
||||
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
|
||||
>
|
||||
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
|
||||
{render_slot(@inner_block)}
|
||||
</.link>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
@ -581,15 +400,15 @@ defmodule MvWeb.CoreComponents do
|
|||
width, height, and background color classes.
|
||||
|
||||
Icons are extracted from the `deps/heroicons` directory and bundled within
|
||||
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
|
||||
your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
|
||||
|
||||
## Examples
|
||||
|
||||
<.icon name="hero-x-mark-solid" />
|
||||
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
|
||||
<.icon name="hero-x-mark" />
|
||||
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
||||
"""
|
||||
attr :name, :string, required: true
|
||||
attr :class, :string, default: nil
|
||||
attr :class, :string, default: "size-4"
|
||||
|
||||
def icon(%{name: "hero-" <> _} = assigns) do
|
||||
~H"""
|
||||
|
|
@ -604,7 +423,7 @@ defmodule MvWeb.CoreComponents do
|
|||
to: selector,
|
||||
time: 300,
|
||||
transition:
|
||||
{"transition-all transform ease-out duration-300",
|
||||
{"transition-all ease-out duration-300",
|
||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
||||
"opacity-100 translate-y-0 sm:scale-100"}
|
||||
)
|
||||
|
|
@ -615,37 +434,11 @@ defmodule MvWeb.CoreComponents do
|
|||
to: selector,
|
||||
time: 200,
|
||||
transition:
|
||||
{"transition-all transform ease-in duration-200",
|
||||
"opacity-100 translate-y-0 sm:scale-100",
|
||||
{"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
|
||||
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
|
||||
)
|
||||
end
|
||||
|
||||
def show_modal(js \\ %JS{}, id) when is_binary(id) do
|
||||
js
|
||||
|> JS.show(to: "##{id}")
|
||||
|> JS.show(
|
||||
to: "##{id}-bg",
|
||||
time: 300,
|
||||
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
|
||||
)
|
||||
|> show("##{id}-container")
|
||||
|> JS.add_class("overflow-hidden", to: "body")
|
||||
|> JS.focus_first(to: "##{id}-content")
|
||||
end
|
||||
|
||||
def hide_modal(js \\ %JS{}, id) do
|
||||
js
|
||||
|> JS.hide(
|
||||
to: "##{id}-bg",
|
||||
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
|
||||
)
|
||||
|> hide("##{id}-container")
|
||||
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|
||||
|> JS.remove_class("overflow-hidden", to: "body")
|
||||
|> JS.pop_focus()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Translates an error message using gettext.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -4,11 +4,86 @@ defmodule MvWeb.Layouts do
|
|||
|
||||
See the `layouts` directory for all templates available.
|
||||
The "root" layout is a skeleton rendered as part of the
|
||||
application router. The "app" layout is set as the default
|
||||
layout on both `use MvWeb, :controller` and
|
||||
`use MvWeb, :live_view`.
|
||||
application router. The "app" layout is rendered as component
|
||||
in regular views and live views.
|
||||
"""
|
||||
use MvWeb, :html
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
import MvWeb.Layouts.Navbar
|
||||
|
||||
embed_templates "layouts/*"
|
||||
|
||||
@doc """
|
||||
Renders the app layout
|
||||
|
||||
## Examples
|
||||
|
||||
<Layouts.app flash={@flash}>
|
||||
<h1>Content</h1>
|
||||
</Layout.app>
|
||||
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
|
||||
attr :current_scope, :map,
|
||||
default: nil,
|
||||
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<.navbar />
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-16">
|
||||
<div class="mx-auto max-full space-y-4">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shows the flash group with standard titles and content.
|
||||
|
||||
## Examples
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
attr :flash, :map, required: true, doc: "the map of flash messages"
|
||||
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
||||
|
||||
def flash_group(assigns) do
|
||||
~H"""
|
||||
<div id={@id} aria-live="polite">
|
||||
<.flash kind={:info} flash={@flash} />
|
||||
<.flash kind={:error} flash={@flash} />
|
||||
|
||||
<.flash
|
||||
id="client-error"
|
||||
kind={:error}
|
||||
title={gettext("We can't find the internet")}
|
||||
phx-disconnected={show(".phx-client-error #client-error") |> JS.remove_attribute("hidden")}
|
||||
phx-connected={hide("#client-error") |> JS.set_attribute({"hidden", ""})}
|
||||
hidden
|
||||
>
|
||||
{gettext("Attempting to reconnect")}
|
||||
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
||||
</.flash>
|
||||
|
||||
<.flash
|
||||
id="server-error"
|
||||
kind={:error}
|
||||
title={gettext("Something went wrong!")}
|
||||
phx-disconnected={show(".phx-server-error #server-error") |> JS.remove_attribute("hidden")}
|
||||
phx-connected={hide("#server-error") |> JS.set_attribute({"hidden", ""})}
|
||||
hidden
|
||||
>
|
||||
{gettext("Attempting to reconnect")}
|
||||
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
||||
</.flash>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
<header class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/">
|
||||
<img src={~p"/images/logo.svg"} width="36" />
|
||||
</a>
|
||||
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
|
||||
v{Application.spec(:phoenix, :vsn)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
|
||||
<form method="post" action="/set_locale" class="mr-4">
|
||||
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
|
||||
<select name="locale" onchange="this.form.submit()" class="rounded border px-2 py-1">
|
||||
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
|
||||
@elixirphoenix
|
||||
</a>
|
||||
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://hexdocs.pm/phoenix/overview.html"
|
||||
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
|
||||
>
|
||||
Get Started <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<.flash_group flash={@flash} />
|
||||
{@inner_content}
|
||||
</div>
|
||||
</main>
|
||||
80
lib/mv_web/components/layouts/navbar.ex
Normal file
80
lib/mv_web/components/layouts/navbar.ex
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
defmodule MvWeb.Layouts.Navbar do
|
||||
@moduledoc """
|
||||
Navbar that is used in the rootlayout shown on every page
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
def navbar(assigns) do
|
||||
~H"""
|
||||
<header class="navbar bg-base-100 shadow-sm">
|
||||
<div class="flex-1">
|
||||
<a class="btn btn-ghost text-xl">Mitgliederverwaltung</a>
|
||||
<ul class="menu menu-horizontal bg-base-200">
|
||||
<li><a href="/members">{gettext("Members")}</a></li>
|
||||
<li><a>Transaktionen</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<form method="post" action="/set_locale" class="mr-4">
|
||||
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
|
||||
<select name="locale" onchange="this.form.submit()" class="select select-sm">
|
||||
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
|
||||
<option value="en" selected={Gettext.get_locale() == "en"}>English</option>
|
||||
</select>
|
||||
</form>
|
||||
<!-- Daisy UI Theme Toggle for dark and light mode-->
|
||||
<label class="flex cursor-pointer gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
|
||||
</svg>
|
||||
<input type="checkbox" value="dark" class="toggle theme-controller" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</label>
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar avatar-placeholder">
|
||||
<div class="bg-neutral text-neutral-content w-12 rounded-full">
|
||||
<span>AA</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<a>
|
||||
{gettext("Profil")}
|
||||
</a>
|
||||
</li>
|
||||
<li><a>{gettext("Settings")}</a></li>
|
||||
<li><a href="sign-out">{gettext("Logout")}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="[scrollbar-gutter:stable]">
|
||||
<html lang="en">
|
||||
<head>
|
||||
{Application.get_env(:live_debugger, :live_debugger_tags)}
|
||||
|
||||
|
|
@ -9,11 +9,29 @@
|
|||
<.live_title default="Mv" suffix=" · Phoenix Framework">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
|
||||
</script>
|
||||
<script>
|
||||
(() => {
|
||||
const setTheme = (theme) => {
|
||||
if (theme === "system") {
|
||||
localStorage.removeItem("phx:theme");
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
} else {
|
||||
localStorage.setItem("phx:theme", theme);
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
};
|
||||
if (!document.documentElement.hasAttribute("data-theme")) {
|
||||
setTheme(localStorage.getItem("phx:theme") || "system");
|
||||
}
|
||||
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
|
||||
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-white">
|
||||
<body>
|
||||
{@inner_content}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
44
lib/mv_web/components/table_components.ex
Normal file
44
lib/mv_web/components/table_components.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule MvWeb.TableComponents do
|
||||
@moduledoc """
|
||||
TableComponents that can be used in tables as components (like a button for sorting, a filter...)
|
||||
"""
|
||||
use Phoenix.Component
|
||||
import MvWeb.CoreComponents
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
attr :field, :atom, required: true
|
||||
attr :label, :string, required: true
|
||||
attr :sort_field, :atom, default: nil
|
||||
attr :sort_order, :atom, default: nil
|
||||
|
||||
@doc """
|
||||
A sort button (with chevron icon) that can be used to sort a list of items
|
||||
"""
|
||||
def sort_button(assigns) do
|
||||
~H"""
|
||||
<button
|
||||
type="button"
|
||||
phx-click="sort"
|
||||
phx-value-field={@field}
|
||||
aria-sort={aria_sort(@sort_field, @sort_order, @field)}
|
||||
class="flex items-center gap-1 hover:underline focus:outline-none"
|
||||
>
|
||||
<span>{@label}</span>
|
||||
<%= if @sort_field == @field do %>
|
||||
<.icon name={if @sort_order == :asc, do: "hero-chevron-up", else: "hero-chevron-down"} />
|
||||
<span class="sr-only">
|
||||
({(@sort_order == :asc && gettext("ascending")) || gettext("descending")})
|
||||
</span>
|
||||
<% end %>
|
||||
</button>
|
||||
"""
|
||||
end
|
||||
|
||||
defp aria_sort(current_field, current_order, this_field) do
|
||||
cond do
|
||||
current_field != this_field -> "none"
|
||||
current_order == :asc -> "ascending"
|
||||
true -> "descending"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,8 +2,6 @@ defmodule MvWeb.PageController do
|
|||
use MvWeb, :controller
|
||||
|
||||
def home(conn, _params) do
|
||||
# The home page is often custom made,
|
||||
# so skip the default app layout.
|
||||
render(conn, :home, layout: false)
|
||||
render(conn, :home)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ defmodule MvWeb.Endpoint do
|
|||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
# You should set gzip to true if you are running phx.digest
|
||||
# when deploying your static files in production.
|
||||
# When code reloading is disabled (e.g., in production),
|
||||
# the `gzip` option is enabled to serve compressed
|
||||
# static files generated by running `phx.digest`.
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
from: :mv,
|
||||
gzip: false,
|
||||
gzip: not code_reloading?,
|
||||
only: MvWeb.static_paths()
|
||||
|
||||
if Code.ensure_loaded?(Tidewave) do
|
||||
|
|
|
|||
|
|
@ -1,43 +1,18 @@
|
|||
defmodule MvWeb.MemberLive.FormComponent do
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, property_types} = Mv.Membership.list_property_types()
|
||||
|
||||
initial_properties =
|
||||
Enum.map(property_types, fn pt ->
|
||||
%{
|
||||
"property_type_id" => pt.id,
|
||||
"value" => %{
|
||||
"type" => pt.value_type,
|
||||
"value" => nil,
|
||||
"_union_type" => Atom.to_string(pt.value_type)
|
||||
}
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, assign(socket, property_types: property_types, initial_properties: initial_properties)}
|
||||
end
|
||||
defmodule MvWeb.MemberLive.Form do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@title}
|
||||
{@page_title}
|
||||
<:subtitle>
|
||||
{gettext("Use this form to manage member records and their properties.")}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.simple_form
|
||||
for={@form}
|
||||
id="member-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:first_name]} label={gettext("First Name")} required />
|
||||
<.input field={@form[:last_name]} label={gettext("Last Name")} required />
|
||||
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
||||
|
|
@ -71,22 +46,53 @@ defmodule MvWeb.MemberLive.FormComponent do
|
|||
/>
|
||||
</.inputs_for>
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with={gettext("Saving...")}>{gettext("Save Member")}</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
</div>
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Member")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @member)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
def mount(params, _session, socket) do
|
||||
{:ok, property_types} = Mv.Membership.list_property_types()
|
||||
|
||||
initial_properties =
|
||||
Enum.map(property_types, fn pt ->
|
||||
%{
|
||||
"property_type_id" => pt.id,
|
||||
"value" => %{
|
||||
"type" => pt.value_type,
|
||||
"value" => nil,
|
||||
"_union_type" => Atom.to_string(pt.value_type)
|
||||
}
|
||||
}
|
||||
end)
|
||||
|
||||
member =
|
||||
case params["id"] do
|
||||
nil -> nil
|
||||
id -> Ash.get!(Mv.Membership.Member, id)
|
||||
end
|
||||
|
||||
action = if is_nil(member), do: "New", else: "Edit"
|
||||
page_title = action <> " " <> "Member"
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|> assign(:property_types, property_types)
|
||||
|> assign(:initial_properties, initial_properties)
|
||||
|> assign(member: member)
|
||||
|> assign(:page_title, page_title)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
defp return_to("show"), do: "show"
|
||||
defp return_to(_), do: "index"
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"member" => member_params}, socket) do
|
||||
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, member_params))}
|
||||
|
|
@ -107,7 +113,7 @@ defmodule MvWeb.MemberLive.FormComponent do
|
|||
socket =
|
||||
socket
|
||||
|> put_flash(:info, gettext("Member %{action} successfully", action: action))
|
||||
|> push_patch(to: socket.assigns.patch)
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, member))
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
|
|
@ -175,4 +181,7 @@ defmodule MvWeb.MemberLive.FormComponent do
|
|||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
|
||||
defp return_path("index", _member), do: ~p"/members"
|
||||
defp return_path("show", member), do: ~p"/members/#{member.id}"
|
||||
end
|
||||
86
lib/mv_web/live/member_live/index.ex
Normal file
86
lib/mv_web/live/member_live/index.ex
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
defmodule MvWeb.MemberLive.Index do
|
||||
use MvWeb, :live_view
|
||||
import MvWeb.TableComponents
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
members = Ash.read!(Mv.Membership.Member)
|
||||
sorted = Enum.sort_by(members, & &1.first_name)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, gettext("Members"))
|
||||
|> assign(:sort_field, :first_name)
|
||||
|> assign(:sort_order, :asc)
|
||||
|> assign(:members, sorted)
|
||||
|> assign(:selected_members, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
member = Ash.get!(Mv.Membership.Member, id)
|
||||
Ash.destroy!(member)
|
||||
|
||||
{:noreply, stream_delete(socket, :members, member)}
|
||||
end
|
||||
|
||||
# Selects one member in the list of members
|
||||
@impl true
|
||||
def handle_event("select_member", %{"id" => id}, socket) do
|
||||
selected =
|
||||
if id in socket.assigns.selected_members do
|
||||
List.delete(socket.assigns.selected_members, id)
|
||||
else
|
||||
[id | socket.assigns.selected_members]
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
# Sorts the list of members according to a field, when you click on the column header
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
members = socket.assigns.members
|
||||
field = String.to_existing_atom(field_str)
|
||||
|
||||
new_order =
|
||||
if socket.assigns.sort_field == field do
|
||||
toggle_order(socket.assigns.sort_order)
|
||||
else
|
||||
:asc
|
||||
end
|
||||
|
||||
sorted_members =
|
||||
members
|
||||
|> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, new_order)
|
||||
|> assign(:members, sorted_members)}
|
||||
end
|
||||
|
||||
# Selects all members in the list of members
|
||||
|
||||
@impl true
|
||||
def handle_event("select_all", _params, socket) do
|
||||
members = socket.assigns.members
|
||||
|
||||
all_ids = Enum.map(members, & &1.id)
|
||||
|
||||
selected =
|
||||
if Enum.sort(socket.assigns.selected_members) == Enum.sort(all_ids) do
|
||||
[]
|
||||
else
|
||||
all_ids
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_members, selected)}
|
||||
end
|
||||
|
||||
defp toggle_order(:asc), do: :desc
|
||||
defp toggle_order(:desc), do: :asc
|
||||
defp sort_fun(:asc), do: &<=/2
|
||||
defp sort_fun(:desc), do: &>=/2
|
||||
end
|
||||
83
lib/mv_web/live/member_live/index.html.heex
Normal file
83
lib/mv_web/live/member_live/index.html.heex
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{gettext("Members")}
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/members/new"}>
|
||||
<.icon name="hero-plus" /> {gettext("New Member")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="members"
|
||||
rows={@members}
|
||||
row_click={fn member -> JS.navigate(~p"/members/#{member}") end}
|
||||
>
|
||||
|
||||
<!-- <:col :let={member} label="Id">{member.id}</:col> -->
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
~H"""
|
||||
<.input
|
||||
type="checkbox"
|
||||
name="select_all"
|
||||
phx-click="select_all"
|
||||
checked={Enum.sort(@selected_members) == Enum.map(@members, & &1.id) |> Enum.sort()}
|
||||
aria-label={gettext("Select all members")}
|
||||
role="checkbox"
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.input
|
||||
type="checkbox"
|
||||
name={member.id}
|
||||
phx-click="select_member"
|
||||
phx-value-id={member.id}
|
||||
checked={member.id in @selected_members}
|
||||
phx-capture-click
|
||||
phx-stop-propagation
|
||||
aria-label={gettext("Select member")}
|
||||
role="checkbox"
|
||||
/>
|
||||
</:col>
|
||||
<:col
|
||||
:let={member}
|
||||
label={
|
||||
sort_button(%{
|
||||
field: :first_name,
|
||||
label: gettext("Name"),
|
||||
sort_field: @sort_field,
|
||||
sort_order: @sort_order
|
||||
})
|
||||
}
|
||||
>
|
||||
{member.first_name} {member.last_name}
|
||||
</:col>
|
||||
<:col :let={member} label={gettext("Email")}>{member.email}</:col>
|
||||
<:col :let={member} label={gettext("Street")}>{member.street}</:col>
|
||||
<:col :let={member} label={gettext("House Number")}>{member.house_number}</:col>
|
||||
<:col :let={member} label={gettext("Postal Code")}>{member.postal_code}</:col>
|
||||
<:col :let={member} label={gettext("City")}>{member.city}</:col>
|
||||
<:col :let={member} label={gettext("Phone Number")}>{member.phone_number}</:col>
|
||||
<:col :let={member} label={gettext("Join Date")}>{member.join_date}</:col>
|
||||
|
||||
<:action :let={member}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/members/#{member}/edit"}>{gettext("Edit")}</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={member}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: member.id}) |> hide("#row-#{member.id}")}
|
||||
data-confirm={gettext("Are you sure?")}
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
82
lib/mv_web/live/member_live/show.ex
Normal file
82
lib/mv_web/live/member_live/show.ex
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
defmodule MvWeb.MemberLive.Show do
|
||||
use MvWeb, :live_view
|
||||
import Ash.Query
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@member.first_name} {@member.last_name}
|
||||
<:subtitle>{gettext("This is a member record from your database.")}</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/members"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> {gettext("Edit Member")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title={gettext("Id")}>{@member.id}</:item>
|
||||
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
||||
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
||||
<:item title={gettext("Email")}>{@member.email}</:item>
|
||||
<:item title={gettext("Birth Date")}>{@member.birth_date}</:item>
|
||||
<:item title={gettext("Paid")}>
|
||||
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
||||
</:item>
|
||||
<:item title={gettext("Phone Number")}>{@member.phone_number}</:item>
|
||||
<:item title={gettext("Join Date")}>{@member.join_date}</:item>
|
||||
<:item title={gettext("Exit Date")}>{@member.exit_date}</:item>
|
||||
<:item title={gettext("Notes")}>{@member.notes}</:item>
|
||||
<:item title={gettext("City")}>{@member.city}</:item>
|
||||
<:item title={gettext("Street")}>{@member.street}</:item>
|
||||
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
||||
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
||||
</.list>
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||
<.generic_list items={
|
||||
Enum.map(@member.properties, fn p ->
|
||||
{
|
||||
# name
|
||||
p.property_type && p.property_type.name,
|
||||
# value
|
||||
case p.value do
|
||||
%{value: v} -> v
|
||||
v -> v
|
||||
end
|
||||
}
|
||||
end)
|
||||
} />
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> filter(id == ^id)
|
||||
|> load(properties: [:property_type])
|
||||
|
||||
member = Ash.read_one!(query)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:member, member)}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: gettext("Show Member")
|
||||
defp page_title(:edit), do: gettext("Edit Member")
|
||||
end
|
||||
251
lib/mv_web/live/property_live/form.ex
Normal file
251
lib/mv_web/live/property_live/form.ex
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
defmodule MvWeb.PropertyLive.Form do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{gettext("Use this form to manage property records in your database.")}</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="property-form" phx-change="validate" phx-submit="save">
|
||||
<!-- Property Type Selection -->
|
||||
<.input
|
||||
field={@form[:property_type_id]}
|
||||
type="select"
|
||||
label={gettext("Property type")}
|
||||
options={property_type_options(@property_types)}
|
||||
prompt={gettext("Choose a property type")}
|
||||
/>
|
||||
|
||||
<!-- Member Selection -->
|
||||
<.input
|
||||
field={@form[:member_id]}
|
||||
type="select"
|
||||
label={gettext("Member")}
|
||||
options={member_options(@members)}
|
||||
prompt={gettext("Choose a member")}
|
||||
/>
|
||||
|
||||
<!-- Value Input - handles Union type -->
|
||||
<%= if @selected_property_type do %>
|
||||
<.union_value_input form={@form} property_type={@selected_property_type} />
|
||||
<% else %>
|
||||
<div class="text-sm text-gray-600">
|
||||
{gettext("Please select a property type first")}
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Property")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @property)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper function for Union-Value Input
|
||||
defp union_value_input(assigns) do
|
||||
# Extract the current value from the Property
|
||||
current_value = extract_current_value(assigns.form.data, assigns.property_type.value_type)
|
||||
assigns = assign(assigns, :current_value, current_value)
|
||||
|
||||
~H"""
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
{gettext("Value")}
|
||||
</label>
|
||||
|
||||
<%= case @property_type.value_type do %>
|
||||
<% :string -> %>
|
||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
||||
<.input field={value_form[:value]} type="text" label="" value={@current_value} />
|
||||
<input type="hidden" name={value_form[:_union_type].name} value="string" />
|
||||
</.inputs_for>
|
||||
<% :integer -> %>
|
||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
||||
<.input field={value_form[:value]} type="number" label="" value={@current_value} />
|
||||
<input type="hidden" name={value_form[:_union_type].name} value="integer" />
|
||||
</.inputs_for>
|
||||
<% :boolean -> %>
|
||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
||||
<.input field={value_form[:value]} type="checkbox" label="" checked={@current_value} />
|
||||
<input type="hidden" name={value_form[:_union_type].name} value="boolean" />
|
||||
</.inputs_for>
|
||||
<% :date -> %>
|
||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
||||
<.input
|
||||
field={value_form[:value]}
|
||||
type="date"
|
||||
label=""
|
||||
value={format_date_value(@current_value)}
|
||||
/>
|
||||
<input type="hidden" name={value_form[:_union_type].name} value="date" />
|
||||
</.inputs_for>
|
||||
<% :email -> %>
|
||||
<.inputs_for :let={value_form} field={@form[:value]}>
|
||||
<.input field={value_form[:value]} type="email" label="" value={@current_value} />
|
||||
<input type="hidden" name={value_form[:_union_type].name} value="email" />
|
||||
</.inputs_for>
|
||||
<% _ -> %>
|
||||
<div class="text-sm text-red-600">
|
||||
{gettext("Unsupported value type: %{type}", type: @property_type.value_type)}
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
# Helper function to extract the current value from the Property
|
||||
defp extract_current_value(
|
||||
%Mv.Membership.Property{value: %Ash.Union{value: value}},
|
||||
_value_type
|
||||
) do
|
||||
value
|
||||
end
|
||||
|
||||
defp extract_current_value(_data, _value_type) do
|
||||
nil
|
||||
end
|
||||
|
||||
# Helper function to format Date values for HTML input
|
||||
defp format_date_value(%Date{} = date) do
|
||||
Date.to_iso8601(date)
|
||||
end
|
||||
|
||||
defp format_date_value(nil), do: ""
|
||||
|
||||
defp format_date_value(date) when is_binary(date) do
|
||||
case Date.from_iso8601(date) do
|
||||
{:ok, parsed_date} -> Date.to_iso8601(parsed_date)
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp format_date_value(_), do: ""
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
property =
|
||||
case params["id"] do
|
||||
nil -> nil
|
||||
id -> Ash.get!(Mv.Membership.Property, id) |> Ash.load!([:property_type])
|
||||
end
|
||||
|
||||
action = if is_nil(property), do: "New", else: "Edit"
|
||||
page_title = action <> " " <> "Property"
|
||||
|
||||
# Load all PropertyTypes and Members for the selection fields
|
||||
property_types = Ash.read!(Mv.Membership.PropertyType)
|
||||
members = Ash.read!(Mv.Membership.Member)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|> assign(property: property)
|
||||
|> assign(:page_title, page_title)
|
||||
|> assign(:property_types, property_types)
|
||||
|> assign(:members, members)
|
||||
|> assign(:selected_property_type, property && property.property_type)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
defp return_to("show"), do: "show"
|
||||
defp return_to(_), do: "index"
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"property" => property_params}, socket) do
|
||||
# Find the selected PropertyType
|
||||
selected_property_type =
|
||||
case property_params["property_type_id"] do
|
||||
"" -> nil
|
||||
nil -> nil
|
||||
id -> Enum.find(socket.assigns.property_types, &(&1.id == id))
|
||||
end
|
||||
|
||||
# Set the Union type based on the selected PropertyType
|
||||
updated_params =
|
||||
if selected_property_type do
|
||||
union_type = to_string(selected_property_type.value_type)
|
||||
put_in(property_params, ["value", "_union_type"], union_type)
|
||||
else
|
||||
property_params
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:selected_property_type, selected_property_type)
|
||||
|> assign(form: AshPhoenix.Form.validate(socket.assigns.form, updated_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"property" => property_params}, socket) do
|
||||
# Set the Union type based on the selected PropertyType
|
||||
updated_params =
|
||||
if socket.assigns.selected_property_type do
|
||||
union_type = to_string(socket.assigns.selected_property_type.value_type)
|
||||
put_in(property_params, ["value", "_union_type"], union_type)
|
||||
else
|
||||
property_params
|
||||
end
|
||||
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: updated_params) do
|
||||
{:ok, property} ->
|
||||
notify_parent({:saved, property})
|
||||
|
||||
action =
|
||||
case socket.assigns.form.source.type do
|
||||
:create -> gettext("create")
|
||||
:update -> gettext("update")
|
||||
other -> to_string(other)
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, gettext("Property %{action} successfully", action: action))
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, property))
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{property: property}} = socket) do
|
||||
form =
|
||||
if property do
|
||||
# Determine the Union type based on the property_type
|
||||
union_type = property.property_type && property.property_type.value_type
|
||||
|
||||
params =
|
||||
if union_type do
|
||||
%{"value" => %{"_union_type" => to_string(union_type)}}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
AshPhoenix.Form.for_update(property, :update, as: "property", params: params)
|
||||
else
|
||||
AshPhoenix.Form.for_create(Mv.Membership.Property, :create, as: "property")
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
|
||||
defp return_path("index", _property), do: ~p"/properties"
|
||||
defp return_path("show", property), do: ~p"/properties/#{property.id}"
|
||||
|
||||
# Helper functions for selection options
|
||||
defp property_type_options(property_types) do
|
||||
Enum.map(property_types, &{&1.name, &1.id})
|
||||
end
|
||||
|
||||
defp member_options(members) do
|
||||
Enum.map(members, &{"#{&1.first_name} #{&1.last_name}", &1.id})
|
||||
end
|
||||
end
|
||||
60
lib/mv_web/live/property_live/index.ex
Normal file
60
lib/mv_web/live/property_live/index.ex
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
defmodule MvWeb.PropertyLive.Index do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Listing Properties
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/properties/new"}>
|
||||
<.icon name="hero-plus" /> New Property
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="properties"
|
||||
rows={@streams.properties}
|
||||
row_click={fn {_id, property} -> JS.navigate(~p"/properties/#{property}") end}
|
||||
>
|
||||
<:col :let={{_id, property}} label="Id">{property.id}</:col>
|
||||
|
||||
<:action :let={{_id, property}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/properties/#{property}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/properties/#{property}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Properties")
|
||||
|> stream(:properties, Ash.read!(Mv.Membership.Property))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property = Ash.get!(Mv.Membership.Property, id)
|
||||
Ash.destroy!(property)
|
||||
|
||||
{:noreply, stream_delete(socket, :properties, property)}
|
||||
end
|
||||
end
|
||||
44
lib/mv_web/live/property_live/show.ex
Normal file
44
lib/mv_web/live/property_live/show.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
defmodule MvWeb.PropertyLive.Show do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Property {@property.id}
|
||||
<:subtitle>This is a property record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/properties"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/properties/#{@property}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> Edit Property
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property.id}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:property, Ash.get!(Mv.Membership.Property, id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Property"
|
||||
defp page_title(:edit), do: "Edit Property"
|
||||
end
|
||||
105
lib/mv_web/live/property_type_live/form.ex
Normal file
105
lib/mv_web/live/property_type_live/form.ex
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Form do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>
|
||||
{gettext("Use this form to manage property_type records in your database.")}
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="property_type-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:name]} type="text" label={gettext("Name")} />
|
||||
<.input
|
||||
field={@form[:value_type]}
|
||||
type="select"
|
||||
label={gettext("Value type")}
|
||||
options={
|
||||
Ash.Resource.Info.attribute(Mv.Membership.PropertyType, :value_type).constraints[:one_of]
|
||||
}
|
||||
/>
|
||||
<.input field={@form[:description]} type="text" label={gettext("Description")} />
|
||||
<.input field={@form[:immutable]} type="checkbox" label={gettext("Immutable")} />
|
||||
<.input field={@form[:required]} type="checkbox" label={gettext("Required")} />
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save Property type")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @property_type)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
property_type =
|
||||
case params["id"] do
|
||||
nil -> nil
|
||||
id -> Ash.get!(Mv.Membership.PropertyType, id)
|
||||
end
|
||||
|
||||
action = if is_nil(property_type), do: "New", else: "Edit"
|
||||
page_title = action <> " " <> "Property type"
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|> assign(property_type: property_type)
|
||||
|> assign(:page_title, page_title)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
defp return_to("show"), do: "show"
|
||||
defp return_to(_), do: "index"
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"property_type" => property_type_params}, socket) do
|
||||
{:noreply,
|
||||
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, property_type_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"property_type" => property_type_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: property_type_params) do
|
||||
{:ok, property_type} ->
|
||||
notify_parent({:saved, property_type})
|
||||
|
||||
action =
|
||||
case socket.assigns.form.source.type do
|
||||
:create -> gettext("create")
|
||||
:update -> gettext("update")
|
||||
other -> to_string(other)
|
||||
end
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, gettext("Property type %{action} successfully", action: action))
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, property_type))
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{property_type: property_type}} = socket) do
|
||||
form =
|
||||
if property_type do
|
||||
AshPhoenix.Form.for_update(property_type, :update, as: "property_type")
|
||||
else
|
||||
AshPhoenix.Form.for_create(Mv.Membership.PropertyType, :create, as: "property_type")
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
|
||||
defp return_path("index", _property_type), do: ~p"/property_types"
|
||||
defp return_path("show", property_type), do: ~p"/property_types/#{property_type.id}"
|
||||
end
|
||||
64
lib/mv_web/live/property_type_live/index.ex
Normal file
64
lib/mv_web/live/property_type_live/index.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Index do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Listing Property types
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/property_types/new"}>
|
||||
<.icon name="hero-plus" /> New Property type
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="property_types"
|
||||
rows={@streams.property_types}
|
||||
row_click={fn {_id, property_type} -> JS.navigate(~p"/property_types/#{property_type}") end}
|
||||
>
|
||||
<:col :let={{_id, property_type}} label="Id">{property_type.id}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Name">{property_type.name}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Description">{property_type.description}</:col>
|
||||
|
||||
<:action :let={{_id, property_type}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/property_types/#{property_type}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/property_types/#{property_type}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property_type}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Listing Property types")
|
||||
|> stream(:property_types, Ash.read!(Mv.Membership.PropertyType))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property_type = Ash.get!(Mv.Membership.PropertyType, id)
|
||||
Ash.destroy!(property_type)
|
||||
|
||||
{:noreply, stream_delete(socket, :property_types, property_type)}
|
||||
end
|
||||
end
|
||||
43
lib/mv_web/live/property_type_live/show.ex
Normal file
43
lib/mv_web/live/property_type_live/show.ex
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Show do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Property type {@property_type.id}
|
||||
<:subtitle>This is a property_type record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/property_types"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button
|
||||
variant="primary"
|
||||
navigate={~p"/property_types/#{@property_type}/edit?return_to=show"}
|
||||
>
|
||||
<.icon name="hero-pencil-square" /> Edit Property type
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property_type.id}</:item>
|
||||
|
||||
<:item title="Name">{@property_type.name}</:item>
|
||||
|
||||
<:item title="Description">{@property_type.description}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Show Property type")
|
||||
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))}
|
||||
end
|
||||
end
|
||||
180
lib/mv_web/live/user_live/form.ex
Normal file
180
lib/mv_web/live/user_live/form.ex
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
defmodule MvWeb.UserLive.Form do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{@page_title}
|
||||
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save">
|
||||
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
|
||||
|
||||
<!-- Password Section -->
|
||||
<div class="mt-6">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="set_password"
|
||||
phx-click="toggle_password_section"
|
||||
checked={@show_password_fields}
|
||||
class="checkbox checkbox-sm"
|
||||
/>
|
||||
<span class="text-sm font-medium">
|
||||
{if @user, do: gettext("Change Password"), else: gettext("Set Password")}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<%= if @show_password_fields do %>
|
||||
<div class="mt-4 space-y-4 p-4 bg-gray-50 rounded-lg">
|
||||
<.input
|
||||
field={@form[:password]}
|
||||
label={gettext("Password")}
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<!-- Only show password confirmation for new users (register_with_password) -->
|
||||
<%= if !@user do %>
|
||||
<.input
|
||||
field={@form[:password_confirmation]}
|
||||
label={gettext("Confirm Password")}
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<% end %>
|
||||
|
||||
<div class="text-sm text-gray-600">
|
||||
<p><strong>{gettext("Password requirements")}:</strong></p>
|
||||
<ul class="list-disc list-inside text-xs mt-1 space-y-1">
|
||||
<li>{gettext("At least 8 characters")}</li>
|
||||
<li>{gettext("Include both letters and numbers")}</li>
|
||||
<li>{gettext("Consider using special characters")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<%= if @user do %>
|
||||
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded">
|
||||
<p class="text-sm text-orange-800">
|
||||
<strong>{gettext("Admin Note")}:</strong> {gettext(
|
||||
"As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= if @user do %>
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
|
||||
<p class="text-sm text-blue-800">
|
||||
<strong>{gettext("Note")}:</strong> {gettext(
|
||||
"Check 'Change Password' above to set a new password for this user."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-4 p-4 bg-yellow-50 rounded-lg">
|
||||
<p class="text-sm text-yellow-800">
|
||||
<strong>{gettext("Note")}:</strong> {gettext(
|
||||
"User will be created without a password. Check 'Set Password' to add one."
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
||||
{gettext("Save User")}
|
||||
</.button>
|
||||
<.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}</.button>
|
||||
</.form>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(params, _session, socket) do
|
||||
user =
|
||||
case params["id"] do
|
||||
nil -> nil
|
||||
id -> Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
|
||||
end
|
||||
|
||||
action = if is_nil(user), do: gettext("New"), else: gettext("Edit")
|
||||
page_title = action <> " " <> gettext("User")
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:return_to, return_to(params["return_to"]))
|
||||
|> assign(user: user)
|
||||
|> assign(:page_title, page_title)
|
||||
|> assign(:show_password_fields, false)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
defp return_to("show"), do: "show"
|
||||
defp return_to(_), do: "index"
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_password_section", _params, socket) do
|
||||
show_password_fields = !socket.assigns.show_password_fields
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:show_password_fields, show_password_fields)
|
||||
|> assign_form()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
{:noreply, assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, user_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: user_params) do
|
||||
{:ok, user} ->
|
||||
notify_parent({:saved, user})
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, "User #{socket.assigns.form.source.type}d successfully")
|
||||
|> push_navigate(to: return_path(socket.assigns.return_to, user))
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{user: user, show_password_fields: show_password_fields}} = socket) do
|
||||
form =
|
||||
if user do
|
||||
# For existing users, use admin password action if password fields are shown
|
||||
action = if show_password_fields, do: :admin_set_password, else: :update_user
|
||||
AshPhoenix.Form.for_update(user, action, domain: Mv.Accounts, as: "user")
|
||||
else
|
||||
# For new users, use password registration if password fields are shown
|
||||
action = if show_password_fields, do: :register_with_password, else: :create_user
|
||||
|
||||
AshPhoenix.Form.for_create(Mv.Accounts.User, action,
|
||||
domain: Mv.Accounts,
|
||||
as: "user"
|
||||
)
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
|
||||
defp return_path("index", _user), do: ~p"/users"
|
||||
defp return_path("show", user), do: ~p"/users/#{user.id}"
|
||||
end
|
||||
86
lib/mv_web/live/user_live/index.ex
Normal file
86
lib/mv_web/live/user_live/index.ex
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
defmodule MvWeb.UserLive.Index do
|
||||
use MvWeb, :live_view
|
||||
import MvWeb.TableComponents
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
users = Ash.read!(Mv.Accounts.User, domain: Mv.Accounts)
|
||||
sorted = Enum.sort_by(users, & &1.email)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, gettext("Listing Users"))
|
||||
|> assign(:sort_field, :email)
|
||||
|> assign(:sort_order, :asc)
|
||||
|> assign(:users, sorted)
|
||||
|> assign(:selected_users, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts)
|
||||
Ash.destroy!(user, domain: Mv.Accounts)
|
||||
|
||||
updated_users = Enum.reject(socket.assigns.users, &(&1.id == id))
|
||||
{:noreply, assign(socket, :users, updated_users)}
|
||||
end
|
||||
|
||||
# Selects one user in the list of users
|
||||
@impl true
|
||||
def handle_event("select_user", %{"id" => id}, socket) do
|
||||
selected =
|
||||
if id in socket.assigns.selected_users do
|
||||
List.delete(socket.assigns.selected_users, id)
|
||||
else
|
||||
[id | socket.assigns.selected_users]
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_users, selected)}
|
||||
end
|
||||
|
||||
# Sorts the list of users according to a field, when you click on the column header
|
||||
@impl true
|
||||
def handle_event("sort", %{"field" => field_str}, socket) do
|
||||
users = socket.assigns.users
|
||||
field = String.to_existing_atom(field_str)
|
||||
|
||||
new_order =
|
||||
if socket.assigns.sort_field == field do
|
||||
toggle_order(socket.assigns.sort_order)
|
||||
else
|
||||
:asc
|
||||
end
|
||||
|
||||
sorted_users =
|
||||
users
|
||||
|> Enum.sort_by(&Map.get(&1, field), sort_fun(new_order))
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:sort_field, field)
|
||||
|> assign(:sort_order, new_order)
|
||||
|> assign(:users, sorted_users)}
|
||||
end
|
||||
|
||||
# Selects all users in the list of users
|
||||
@impl true
|
||||
def handle_event("select_all", _params, socket) do
|
||||
users = socket.assigns.users
|
||||
|
||||
all_ids = Enum.map(users, & &1.id)
|
||||
|
||||
selected =
|
||||
if Enum.sort(socket.assigns.selected_users) == Enum.sort(all_ids) do
|
||||
[]
|
||||
else
|
||||
all_ids
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, :selected_users, selected)}
|
||||
end
|
||||
|
||||
defp toggle_order(:asc), do: :desc
|
||||
defp toggle_order(:desc), do: :asc
|
||||
defp sort_fun(:asc), do: &<=/2
|
||||
defp sort_fun(:desc), do: &>=/2
|
||||
end
|
||||
71
lib/mv_web/live/user_live/index.html.heex
Normal file
71
lib/mv_web/live/user_live/index.html.heex
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{gettext("Listing Users")}
|
||||
<:actions>
|
||||
<.button variant="primary" navigate={~p"/users/new"}>
|
||||
<.icon name="hero-plus" /> {gettext("New User")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table id="users" rows={@users} row_click={fn user -> JS.navigate(~p"/users/#{user}") end}>
|
||||
<:col
|
||||
:let={user}
|
||||
label={
|
||||
~H"""
|
||||
<.input
|
||||
type="checkbox"
|
||||
name="select_all"
|
||||
phx-click="select_all"
|
||||
checked={Enum.sort(@selected_users) == Enum.map(@users, & &1.id) |> Enum.sort()}
|
||||
aria-label={gettext("Select all users")}
|
||||
role="checkbox"
|
||||
/>
|
||||
"""
|
||||
}
|
||||
>
|
||||
<.input
|
||||
type="checkbox"
|
||||
name={user.id}
|
||||
phx-click="select_user"
|
||||
phx-value-id={user.id}
|
||||
checked={user.id in @selected_users}
|
||||
phx-capture-click
|
||||
phx-stop-propagation
|
||||
aria-label={gettext("Select user")}
|
||||
role="checkbox"
|
||||
/>
|
||||
</:col>
|
||||
<:col
|
||||
:let={user}
|
||||
label={
|
||||
sort_button(%{
|
||||
field: :email,
|
||||
label: gettext("Email"),
|
||||
sort_field: @sort_field,
|
||||
sort_order: @sort_order
|
||||
})
|
||||
}
|
||||
>
|
||||
{user.email}
|
||||
</:col>
|
||||
<:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id}</:col>
|
||||
|
||||
<:action :let={user}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/users/#{user}"}>{gettext("Show")}</.link>
|
||||
</div>
|
||||
|
||||
<.link navigate={~p"/users/#{user}/edit"}>{gettext("Edit")}</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={user}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("#row-#{user.id}")}
|
||||
data-confirm={gettext("Are you sure?")}
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
41
lib/mv_web/live/user_live/show.ex
Normal file
41
lib/mv_web/live/user_live/show.ex
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
defmodule MvWeb.UserLive.Show do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
{gettext("User")} {@user.email}
|
||||
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.button navigate={~p"/users"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> {gettext("Edit User")}
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title={gettext("ID")}>{@user.id}</:item>
|
||||
<:item title={gettext("Email")}>{@user.email}</:item>
|
||||
<:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")}</:item>
|
||||
<:item title={gettext("Password Authentication")}>
|
||||
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
|
||||
</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, gettext("Show User"))
|
||||
|> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
defmodule MvWeb.MemberLive.Index do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
{gettext("Listing Members")}
|
||||
<:actions>
|
||||
<.link patch={~p"/members/new"}>
|
||||
<.button>{gettext("New Member")}</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="members"
|
||||
rows={@streams.members}
|
||||
row_click={fn {_id, member} -> JS.navigate(~p"/members/#{member}") end}
|
||||
>
|
||||
<!-- <:col :let={{_id, member}} label="Id">{member.id}</:col> -->
|
||||
<:col :let={{_id, member}} label={gettext("First Name")}>{member.first_name}</:col>
|
||||
<:col :let={{_id, member}} label={gettext("Last Name")}>{member.last_name}</:col>
|
||||
<:col :let={{_id, member}} label={gettext("Email")}>{member.email}</:col>
|
||||
<:col :let={{_id, member}} label={gettext("City")}>{member.city}</:col>
|
||||
<:col :let={{_id, member}} label={gettext("Join Date")}>{member.join_date}</:col>
|
||||
|
||||
<:action :let={{_id, member}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/members/#{member}"}>{gettext("Show")}</.link>
|
||||
</div>
|
||||
|
||||
<.link patch={~p"/members/#{member}/edit"}>{gettext("Edit")}</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, member}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: member.id}) |> hide("##{id}")}
|
||||
data-confirm={gettext("Are you sure?")}
|
||||
>
|
||||
{gettext("Delete")}
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:new, :edit]}
|
||||
id="member-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/members")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.MemberLive.FormComponent}
|
||||
id={(@member && @member.id) || :new}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
member={@member}
|
||||
patch={~p"/members"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, stream(socket, :members, Ash.read!(Mv.Membership.Member))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, gettext("Edit Member"))
|
||||
|> assign(:member, Ash.get!(Mv.Membership.Member, id))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, gettext("New Member"))
|
||||
|> assign(:member, nil)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, gettext("Listing Members"))
|
||||
|> assign(:member, nil)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({MvWeb.MemberLive.FormComponent, {:saved, member}}, socket) do
|
||||
{:noreply, stream_insert(socket, :members, member)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
member = Ash.get!(Mv.Membership.Member, id)
|
||||
Ash.destroy!(member)
|
||||
|
||||
{:noreply, stream_delete(socket, :members, member)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
defmodule MvWeb.MemberLive.Show do
|
||||
use MvWeb, :live_view
|
||||
import Ash.Query
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
{@member.first_name} {@member.last_name}
|
||||
<:subtitle>{gettext("This is a member record from your database.")}</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.link patch={~p"/members/#{@member}/show/edit"} phx-click={JS.push_focus()}>
|
||||
<.button>{gettext("Edit member")}</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title={gettext("Id")}>{@member.id}</:item>
|
||||
<:item title={gettext("First Name")}>{@member.first_name}</:item>
|
||||
<:item title={gettext("Last Name")}>{@member.last_name}</:item>
|
||||
<:item title={gettext("Email")}>{@member.email}</:item>
|
||||
<:item title={gettext("Birth Date")}>{@member.birth_date}</:item>
|
||||
<:item title={gettext("Paid")}>
|
||||
{if @member.paid, do: gettext("Yes"), else: gettext("No")}
|
||||
</:item>
|
||||
<:item title={gettext("Phone Number")}>{@member.phone_number}</:item>
|
||||
<:item title={gettext("Join Date")}>{@member.join_date}</:item>
|
||||
<:item title={gettext("Exit Date")}>{@member.exit_date}</:item>
|
||||
<:item title={gettext("Notes")}>{@member.notes}</:item>
|
||||
<:item title={gettext("City")}>{@member.city}</:item>
|
||||
<:item title={gettext("Street")}>{@member.street}</:item>
|
||||
<:item title={gettext("House Number")}>{@member.house_number}</:item>
|
||||
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
|
||||
</.list>
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
|
||||
<.generic_list items={
|
||||
Enum.map(@member.properties, fn p ->
|
||||
{
|
||||
# name
|
||||
p.property_type && p.property_type.name,
|
||||
# value
|
||||
case p.value do
|
||||
%{value: v} -> v
|
||||
v -> v
|
||||
end
|
||||
}
|
||||
end)
|
||||
} />
|
||||
<.back navigate={~p"/members"}>{gettext("Back to members")}</.back>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :edit}
|
||||
id="member-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/members/#{@member}")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.MemberLive.FormComponent}
|
||||
id={@member.id}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
member={@member}
|
||||
patch={~p"/members/#{@member}"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> filter(id == ^id)
|
||||
|> load(properties: [:property_type])
|
||||
|
||||
member = Ash.read_one!(query)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:member, member)}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: gettext("Show Member")
|
||||
defp page_title(:edit), do: gettext("Edit Member")
|
||||
end
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
defmodule MvWeb.PropertyLive.FormComponent do
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.header>
|
||||
{@title}
|
||||
<:subtitle>Use this form to manage property records in your database.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.simple_form
|
||||
for={@form}
|
||||
id="property-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<.input field={@form[:value]} type="text" label="Value" /><.input
|
||||
field={@form[:member_id]}
|
||||
type="text"
|
||||
label="Member"
|
||||
/><.input field={@form[:property_type_id]} type="text" label="Property type" />
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with="Saving...">Save Property</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"property" => property_params}, socket) do
|
||||
{:noreply,
|
||||
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, property_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"property" => property_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: property_params) do
|
||||
{:ok, property} ->
|
||||
notify_parent({:saved, property})
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, "Property #{socket.assigns.form.source.type}d successfully")
|
||||
|> push_patch(to: socket.assigns.patch)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{property: property}} = socket) do
|
||||
form =
|
||||
if property do
|
||||
AshPhoenix.Form.for_update(property, :update, as: "property")
|
||||
else
|
||||
AshPhoenix.Form.for_create(Mv.Membership.Property, :create, as: "property")
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
end
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
defmodule MvWeb.PropertyLive.Index do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
Listing Properties
|
||||
<:actions>
|
||||
<.link patch={~p"/properties/new"}>
|
||||
<.button>New Property</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="properties"
|
||||
rows={@streams.properties}
|
||||
row_click={fn {_id, property} -> JS.navigate(~p"/properties/#{property}") end}
|
||||
>
|
||||
<:col :let={{_id, property}} label="Id">{property.id}</:col>
|
||||
|
||||
<:action :let={{_id, property}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/properties/#{property}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link patch={~p"/properties/#{property}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:new, :edit]}
|
||||
id="property-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/properties")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.PropertyLive.FormComponent}
|
||||
id={(@property && @property.id) || :new}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
property={@property}
|
||||
patch={~p"/properties"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, stream(socket, :properties, Ash.read!(Mv.Membership.Property))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, "Edit Property")
|
||||
|> assign(:property, Ash.get!(Mv.Membership.Property, id))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "New Property")
|
||||
|> assign(:property, nil)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Properties")
|
||||
|> assign(:property, nil)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({MvWeb.PropertyLive.FormComponent, {:saved, property}}, socket) do
|
||||
{:noreply, stream_insert(socket, :properties, property)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property = Ash.get!(Mv.Membership.Property, id)
|
||||
Ash.destroy!(property)
|
||||
|
||||
{:noreply, stream_delete(socket, :properties, property)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
defmodule MvWeb.PropertyLive.Show do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
Property {@property.id}
|
||||
<:subtitle>This is a property record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.link patch={~p"/properties/#{@property}/show/edit"} phx-click={JS.push_focus()}>
|
||||
<.button>Edit property</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property.id}</:item>
|
||||
</.list>
|
||||
|
||||
<.back navigate={~p"/properties"}>Back to properties</.back>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :edit}
|
||||
id="property-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/properties/#{@property}")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.PropertyLive.FormComponent}
|
||||
id={@property.id}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
property={@property}
|
||||
patch={~p"/properties/#{@property}"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:property, Ash.get!(Mv.Membership.Property, id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Property"
|
||||
defp page_title(:edit), do: "Edit Property"
|
||||
end
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
defmodule MvWeb.PropertyTypeLive.FormComponent do
|
||||
use MvWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<.header>
|
||||
{@title}
|
||||
<:subtitle>Use this form to manage property_type records in your database.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.simple_form
|
||||
for={@form}
|
||||
id="property_type-form"
|
||||
phx-target={@myself}
|
||||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<.input field={@form[:name]} type="text" label="Name" /><.input
|
||||
field={@form[:type]}
|
||||
type="text"
|
||||
label="Type"
|
||||
/><.input field={@form[:description]} type="text" label="Description" /><.input
|
||||
field={@form[:immutable]}
|
||||
type="checkbox"
|
||||
label="Immutable"
|
||||
/><.input field={@form[:required]} type="checkbox" label="Required" />
|
||||
|
||||
<:actions>
|
||||
<.button phx-disable-with="Saving...">Save Property type</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign_form()}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"property_type" => property_type_params}, socket) do
|
||||
{:noreply,
|
||||
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, property_type_params))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"property_type" => property_type_params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.form, params: property_type_params) do
|
||||
{:ok, property_type} ->
|
||||
notify_parent({:saved, property_type})
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> put_flash(:info, "Property type #{socket.assigns.form.source.type}d successfully")
|
||||
|> push_patch(to: socket.assigns.patch)
|
||||
|
||||
{:noreply, socket}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
|
||||
|
||||
defp assign_form(%{assigns: %{property_type: property_type}} = socket) do
|
||||
form =
|
||||
if property_type do
|
||||
AshPhoenix.Form.for_update(property_type, :update, as: "property_type")
|
||||
else
|
||||
AshPhoenix.Form.for_create(Mv.Membership.PropertyType, :create, as: "property_type")
|
||||
end
|
||||
|
||||
assign(socket, form: to_form(form))
|
||||
end
|
||||
end
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Index do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
Listing Property types
|
||||
<:actions>
|
||||
<.link patch={~p"/property_types/new"}>
|
||||
<.button>New Property type</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="property_types"
|
||||
rows={@streams.property_types}
|
||||
row_click={fn {_id, property_type} -> JS.navigate(~p"/property_types/#{property_type}") end}
|
||||
>
|
||||
<:col :let={{_id, property_type}} label="Id">{property_type.id}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Name">{property_type.name}</:col>
|
||||
|
||||
<:col :let={{_id, property_type}} label="Description">{property_type.description}</:col>
|
||||
|
||||
<:action :let={{_id, property_type}}>
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/property_types/#{property_type}"}>Show</.link>
|
||||
</div>
|
||||
|
||||
<.link patch={~p"/property_types/#{property_type}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
|
||||
<:action :let={{id, property_type}}>
|
||||
<.link
|
||||
phx-click={JS.push("delete", value: %{id: property_type.id}) |> hide("##{id}")}
|
||||
data-confirm="Are you sure?"
|
||||
>
|
||||
Delete
|
||||
</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:new, :edit]}
|
||||
id="property_type-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/property_types")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.PropertyTypeLive.FormComponent}
|
||||
id={(@property_type && @property_type.id) || :new}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
property_type={@property_type}
|
||||
patch={~p"/property_types"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, stream(socket, :property_types, Ash.read!(Mv.Membership.PropertyType))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, "Edit Property type")
|
||||
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "New Property type")
|
||||
|> assign(:property_type, nil)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Property types")
|
||||
|> assign(:property_type, nil)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({MvWeb.PropertyTypeLive.FormComponent, {:saved, property_type}}, socket) do
|
||||
{:noreply, stream_insert(socket, :property_types, property_type)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
property_type = Ash.get!(Mv.Membership.PropertyType, id)
|
||||
Ash.destroy!(property_type)
|
||||
|
||||
{:noreply, stream_delete(socket, :property_types, property_type)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
defmodule MvWeb.PropertyTypeLive.Show do
|
||||
use MvWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
Property type {@property_type.id}
|
||||
<:subtitle>This is a property_type record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
<.link patch={~p"/property_types/#{@property_type}/show/edit"} phx-click={JS.push_focus()}>
|
||||
<.button>Edit property_type</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Id">{@property_type.id}</:item>
|
||||
|
||||
<:item title="Name">{@property_type.name}</:item>
|
||||
|
||||
<:item title="Description">{@property_type.description}</:item>
|
||||
</.list>
|
||||
|
||||
<.back navigate={~p"/property_types"}>Back to property_types</.back>
|
||||
|
||||
<.modal
|
||||
:if={@live_action == :edit}
|
||||
id="property_type-modal"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/property_types/#{@property_type}")}
|
||||
>
|
||||
<.live_component
|
||||
module={MvWeb.PropertyTypeLive.FormComponent}
|
||||
id={@property_type.id}
|
||||
title={@page_title}
|
||||
action={@live_action}
|
||||
property_type={@property_type}
|
||||
patch={~p"/property_types/#{@property_type}"}
|
||||
/>
|
||||
</.modal>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:property_type, Ash.get!(Mv.Membership.PropertyType, id))}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Property type"
|
||||
defp page_title(:edit), do: "Edit Property type"
|
||||
end
|
||||
|
|
@ -50,23 +50,29 @@ defmodule MvWeb.Router do
|
|||
get "/", PageController, :home
|
||||
|
||||
live "/members", MemberLive.Index, :index
|
||||
live "/members/new", MemberLive.Index, :new
|
||||
live "/members/:id/edit", MemberLive.Index, :edit
|
||||
live "/members/new", MemberLive.Form, :new
|
||||
live "/members/:id/edit", MemberLive.Form, :edit
|
||||
live "/members/:id", MemberLive.Show, :show
|
||||
live "/members/:id/show/edit", MemberLive.Show, :edit
|
||||
|
||||
live "/property_types", PropertyTypeLive.Index, :index
|
||||
live "/property_types/new", PropertyTypeLive.Index, :new
|
||||
live "/property_types/:id/edit", PropertyTypeLive.Index, :edit
|
||||
live "/property_types/new", PropertyTypeLive.Form, :new
|
||||
live "/property_types/:id/edit", PropertyTypeLive.Form, :edit
|
||||
live "/property_types/:id", PropertyTypeLive.Show, :show
|
||||
live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit
|
||||
|
||||
live "/properties", PropertyLive.Index, :index
|
||||
live "/properties/new", PropertyLive.Index, :new
|
||||
live "/properties/:id/edit", PropertyLive.Index, :edit
|
||||
live "/properties/new", PropertyLive.Form, :new
|
||||
live "/properties/:id/edit", PropertyLive.Form, :edit
|
||||
live "/properties/:id", PropertyLive.Show, :show
|
||||
live "/properties/:id/show/edit", PropertyLive.Show, :edit
|
||||
|
||||
live "/users", UserLive.Index, :index
|
||||
live "/users/new", UserLive.Form, :new
|
||||
live "/users/:id/edit", UserLive.Form, :edit
|
||||
live "/users/:id", UserLive.Show, :show
|
||||
live "/users/:id/show/edit", UserLive.Show, :edit
|
||||
|
||||
post "/set_locale", LocaleController, :set_locale
|
||||
end
|
||||
|
||||
|
|
|
|||
24
mix.exs
24
mix.exs
|
|
@ -5,12 +5,14 @@ defmodule Mv.MixProject do
|
|||
[
|
||||
app: :mv,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.14",
|
||||
elixir: "~> 1.15",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
consolidate_protocols: Mix.env() != :dev,
|
||||
compilers: [:phoenix_live_view] ++ Mix.compilers(),
|
||||
aliases: aliases(),
|
||||
deps: deps()
|
||||
deps: deps(),
|
||||
listeners: [Phoenix.CodeReloader]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -33,9 +35,9 @@ defmodule Mv.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:tidewave, "~> 0.1", only: [:dev]},
|
||||
{:tidewave, "~> 0.2", only: [:dev]},
|
||||
{:sourceror, "~> 1.8", only: [:dev, :test]},
|
||||
{:live_debugger, "~> 0.2", only: [:dev]},
|
||||
{:live_debugger, "~> 0.3", only: [:dev]},
|
||||
{:ash_admin, "~> 0.13"},
|
||||
{:ash_postgres, "~> 2.0"},
|
||||
{:ash_phoenix, "~> 2.0"},
|
||||
|
|
@ -44,17 +46,17 @@ defmodule Mv.MixProject do
|
|||
{:ash_authentication, "~> 4.9"},
|
||||
{:ash_authentication_phoenix, "~> 2.10"},
|
||||
{:igniter, "~> 0.6", only: [:dev, :test]},
|
||||
{:phoenix, "~> 1.7.20"},
|
||||
{:phoenix, "~> 1.8.0-rc.4", override: true},
|
||||
{:phoenix_ecto, "~> 4.5"},
|
||||
{:ecto_sql, "~> 3.10"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 4.1"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 1.0.0"},
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:phoenix_live_view, "~> 1.1.0-rc.3"},
|
||||
{:lazy_html, ">= 0.0.0", only: :test},
|
||||
{:phoenix_live_dashboard, "~> 0.8.3"},
|
||||
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
|
||||
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
|
||||
{:esbuild, "~> 0.9", runtime: Mix.env() == :dev},
|
||||
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
|
||||
{:heroicons,
|
||||
github: "tailwindlabs/heroicons",
|
||||
tag: "v2.2.0",
|
||||
|
|
@ -62,8 +64,8 @@ defmodule Mv.MixProject do
|
|||
app: false,
|
||||
compile: false,
|
||||
depth: 1},
|
||||
{:swoosh, "~> 1.5"},
|
||||
{:finch, "~> 0.13"},
|
||||
{:swoosh, "~> 1.16"},
|
||||
{:req, "~> 0.5"},
|
||||
{:telemetry_metrics, "~> 1.0"},
|
||||
{:telemetry_poller, "~> 1.0"},
|
||||
{:gettext, "~> 0.26"},
|
||||
|
|
|
|||
57
mix.lock
57
mix.lock
|
|
@ -1,45 +1,46 @@
|
|||
%{
|
||||
"ash": {:hex, :ash, "3.5.19", "defd1c6b94475352a7b69f430b792fb64e3a9f7ca030195737bb97dc0f1311b5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.65 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ded976230b1ef823aeb25008cc62de6545bf3ad6208cf1f3badb598fa6c01375"},
|
||||
"ash_admin": {:hex, :ash_admin, "0.13.9", "8a7c0f52be4aa490e4a59137bc40e3abafba9e1977f800bb2edae3f331ef1ebb", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "1373e1749d6b5b21c7ff7d7fc79ac932f6f8d1bd0d154a80758eab168948ea37"},
|
||||
"ash_authentication": {:hex, :ash_authentication, "4.9.3", "2347b7982e3b00ae1165a4ef6875e05540204e933922e302bd3ac2be4c043e20", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "1641988f6c67b7d7517caed9e6cb0f6bd906bbb994e2831022b6ad7cecf45ad0"},
|
||||
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.1", "6facb8e14d7e93c3268b8cb5300d42d3802bd754d241f4215f2c5fc1d34c4c94", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "efc27905b29476cacb67562658d5b38ca0656b3c81c4bcb40a117a2d8d686433"},
|
||||
"ash_phoenix": {:hex, :ash_phoenix, "2.3.6", "c2bea1673af52f305b2fe0c04999bd1f0dc8e127d4757a3d7f42d0b9dea16a7a", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "6923dca70fe1d533864134999f4d9c5c59ef745a6b50982d42d60c18966474cd"},
|
||||
"ash_postgres": {:hex, :ash_postgres, "2.6.6", "f60f806e3e969669329dfd33068bf602f3d7f214e0bbb36c241433f34cbff2e0", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.72 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "3133800432273f9e6effb6f8464fe81da22c5b577aa73291f63fd229f4bb43fb"},
|
||||
"ash_sql": {:hex, :ash_sql, "0.2.80", "7717dca3794d7461b8302b107f039bce2c57773840177528cf94c7c264ed763b", [:mix], [{:ash, "~> 3.5", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "036f96b78bf612a1d1fe798b8795ab1e6ecef81e41ca473b1533b139dd0202ab"},
|
||||
"ash": {:hex, :ash, "3.5.33", "2d4986050ce1c86f711b53f9bb40d6b227871f0cc771dab0b8b814a75a27c5ab", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.65 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c7d1e043059967df749f9445bb903d62ed9c1defb5d45f6ddf32754b411ae93"},
|
||||
"ash_admin": {:hex, :ash_admin, "0.13.13", "d6f491587659c63c1e37b542bdef69c1e2dce9e13696e1fa537488983b98ac10", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "3378f54b5bfdbecc735ed848f137223692be4320975d01c23bd64e47db1f1a9a"},
|
||||
"ash_authentication": {:hex, :ash_authentication, "4.9.9", "23ec61bedc3157c258ece622c6f0f6a7645df275ff5e794d513cc6e8798471eb", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "ab8bd1277ff570425346dcf22dd14a059d9bbce0c28d24964b60e51fabaddda8"},
|
||||
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.5", "9f3b1bee4a57f2269efea61e5efe55472683429b8a5bf1ebdd02d9748640f106", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "3f25778d126c7e759444df0855077802c93299457afdf26566f8de6320ba56da"},
|
||||
"ash_phoenix": {:hex, :ash_phoenix, "2.3.12", "34116f054ca4ef97b4badc73f028d78ee517692b713fd39f4c93f90bc2afd038", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "27394e40b44ca06977e90bd0b38bce7bf41c6dab9fe2aa0b474fdb7c0c1f911b"},
|
||||
"ash_postgres": {:hex, :ash_postgres, "2.6.14", "8085b25864c63029a546ec7191d111f348405cb9d3a90677e52d805576319b55", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.72 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.14 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "0230343b959fb9cd24f76d1ecdbba90045d9625f03b33170ecb7c9ef011c9ac2"},
|
||||
"ash_sql": {:hex, :ash_sql, "0.2.89", "ad4ad497263b586a7f3949ceea5d44620a36cb99a1ef0ff5f58f13a77d9b99ef", [:mix], [{:ash, ">= 3.5.25 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "bd957aee95bbdf6326fc7a9212f9a2ab87329b99ee3646c373a87bb3c9968566"},
|
||||
"assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"},
|
||||
"bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
|
||||
"circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
|
||||
"circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
|
||||
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
|
||||
"db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"},
|
||||
"ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
|
||||
"ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||
"ex_phone_number": {:hex, :ex_phone_number, "0.4.5", "2065cc48c3e9d1ed9821f50877c32f2f6898362cb990f44147ca217c5d1374ed", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "67163f8706f8cbfef1b1f4b9230c461f19786d0d79fd0b22cbeeefc6f0b99d4a"},
|
||||
"ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"},
|
||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
"floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
|
||||
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
|
||||
"fine": {:hex, :fine, "0.1.2", "85cf7dd190c7c6c54c2840754ae977c9acc0417316255b674fad9f2678e4ecc7", [], [], "hexpm", "9113531982c2b60dbea6c7233917ddf16806947cd7104b5d03011bf436ca3072"},
|
||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"igniter": {:hex, :igniter, "0.6.7", "4e183afc59d89289e223c4282fd3e9bb39b82e28d0aa6d3369f70fbd3e21a243", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "43b0a584dc84fd1320772c87047355b604ed2bcdd25392b17f7da8bdd09b61ac"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
"igniter": {:hex, :igniter, "0.6.25", "e2774a4605c2bc9fc38f689232604aea0fc925c7966ae8e928fd9ea2fa9d300c", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "b1916e1e45796d5c371c7671305e81277231617eb58b1c120915aba237fbce6a"},
|
||||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
|
||||
"lazy_html": {:hex, :lazy_html, "0.1.3", "8b9c8c135e95f7bc483de6195c4e1c0b2c913a5e2c57353ef4e82703b7ac8bd1", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "5f96f29587dcfed8a22281e8c44c6607e958ba821d90b9dfc003d1ef610f7d07"},
|
||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||
"live_debugger": {:hex, :live_debugger, "0.2.4", "2e0b02874ca562ba2d8cebb9e024c25c0ae9c1f4ee499135a70814e1dea6183e", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "bfd0db143be54ccf2872f15bfd2209fbec1083d0b06b81b4cedeecb2fa9ac208"},
|
||||
"live_debugger": {:hex, :live_debugger, "0.3.1", "4b4d36481c3b0a49ec082c8268d37974ece34d2091ac323ccc0c906eb0c0d032", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "e50836495134b0dde98dc96919340749130ecb83618ea99d63a3a58ed1dcc27d"},
|
||||
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
|
|
@ -47,41 +48,41 @@
|
|||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.0-rc.4", "6c18c1e07938d3d8dbb957ed0d193fa591718a2997058f6883cfa7447f07612a", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "754c8caf0d1332bc691f826d678b192b3f78cfeb01df2f623683e308b363dc41"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
|
||||
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.0-rc.4", "1e933da296a80c0f57689b25db8711fc47feb452ac5de4b4824e8e64bccae9f9", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4747a143c9b494b19f6ac58b919be46ff773066efe4882ee37ba0fd272f673c2"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||
"plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
|
||||
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"reactor": {:hex, :reactor, "0.15.4", "ef0c56a901c132529a14ab59fed0ccb4fcecb24308fb189a94c908255d4fdafc", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "783bf62fd0c72ded033afabdb8b6190b7048769771a2a97256e6f0bf4fb0a891"},
|
||||
"req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
|
||||
"reactor": {:hex, :reactor, "0.15.6", "d717f9add549b25a089a94c90197718d2d838e35d81dd776b1d81587d4cf2aaa", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "74db98165e3644d86e0f723672d91ceca4339eaa935bcad7e78bf146a46d77b9"},
|
||||
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
|
||||
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"},
|
||||
"sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"},
|
||||
"spark": {:hex, :spark, "2.2.65", "4c10d109c108417ce394158f330be09ef184878bde45de6462397fbda68cec29", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "d66d5070a77f4c69cb4f007e941ac17d5d751ce71190fcd6e6e5fb42ba86f101"},
|
||||
"spark": {:hex, :spark, "2.2.67", "67626cb9f59ea4b1c5aa85d4afdd025e0740cbd49ed82665d0a40ff007d7fd4b", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "c8575402e3afc66871362e821bece890536d16319cdb758c5fb2d1250182e46f"},
|
||||
"spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},
|
||||
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
|
||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.2", "b2325aa7cd2bcd63ba023fa07a73dfc4f80660a592d40912975a879966ed9b7b", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cab7ef7c2c94c68fe21d3da26f6b86db118fdf4e7024ccb5842a4972c1056837"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.5", "5abd71be78302ba21be56a2b68d05c9946ff1f1bd254f949efef09d253b771ac", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c953f51ee0a8b237e0f4307c9cefd3eb1eb751c35fcdda2a8bccb991766473be"},
|
||||
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
|
||||
"tidewave": {:hex, :tidewave, "0.1.7", "a93c500a414cfd211c7058a2b4b22759fb8cde5d72c471a34f7046cd66a5a5e6", [:mix], [{:circular_buffer, "~> 0.4", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.47 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "2cfe9c0c3295132cc682b3cd1c859f801bf2e4d02816618d0659f4d765d26435"},
|
||||
"tidewave": {:hex, :tidewave, "0.2.0", "e98378803e535d3035138e4b354dcfca26b7f862fd44cffef5aa697b814c0b0b", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.47 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "6ad11829f4600cd69955ffc66935e6456b775fea095172147244ba6f65986735"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
|
||||
"ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"},
|
||||
"ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,230 +10,215 @@ msgid ""
|
|||
msgstr ""
|
||||
"Language: en\n"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:482
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
msgstr "Aktionen"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/user_live/index.html.heex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr "Bist du sicher?"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:160
|
||||
#: lib/mv_web/components/layouts.ex:71
|
||||
#: lib/mv_web/components/layouts.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Attempting to reconnect"
|
||||
msgstr "Verbindung wird wiederhergestellt"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:50
|
||||
#: lib/mv_web/member_live/index.ex:25
|
||||
#: lib/mv_web/member_live/show.ex:32
|
||||
#: lib/mv_web/live/member_live/form.ex:25
|
||||
#: lib/mv_web/live/member_live/index.html.heex:62
|
||||
#: lib/mv_web/live/member_live/show.ex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr "Stadt"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:41
|
||||
#: lib/mv_web/live/member_live/index.html.heex:79
|
||||
#: lib/mv_web/live/user_live/index.html.heex:71
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete"
|
||||
msgstr "Löschen"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:33
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr "Bearbeiten"
|
||||
msgstr "Bearbeite"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:76
|
||||
#: lib/mv_web/member_live/show.ex:93
|
||||
#: lib/mv_web/live/member_live/show.ex:18
|
||||
#: lib/mv_web/live/member_live/show.ex:81
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit Member"
|
||||
msgstr "Mitglied bearbeiten"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:43
|
||||
#: lib/mv_web/member_live/index.ex:24
|
||||
#: lib/mv_web/member_live/show.ex:23
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:58
|
||||
#: lib/mv_web/live/member_live/show.ex:27
|
||||
#: lib/mv_web/live/user_live/form.ex:14
|
||||
#: lib/mv_web/live/user_live/index.html.heex:48
|
||||
#: lib/mv_web/live/user_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Email"
|
||||
msgstr "E-Mail"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:151
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Error!"
|
||||
msgstr "Fehler!"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:41
|
||||
#: lib/mv_web/member_live/index.ex:22
|
||||
#: lib/mv_web/member_live/show.ex:21
|
||||
#: lib/mv_web/live/member_live/form.ex:16
|
||||
#: lib/mv_web/live/member_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First Name"
|
||||
msgstr "Vorname"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:172
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Hang in there while we get back on track"
|
||||
msgstr "Bitte warten, wir stellen die Verbindung wieder her."
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:47
|
||||
#: lib/mv_web/member_live/index.ex:26
|
||||
#: lib/mv_web/member_live/show.ex:29
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
msgstr "Beitrittsdatum"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:42
|
||||
#: lib/mv_web/member_live/index.ex:23
|
||||
#: lib/mv_web/member_live/show.ex:22
|
||||
#: lib/mv_web/live/member_live/form.ex:17
|
||||
#: lib/mv_web/live/member_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Last Name"
|
||||
msgstr "Nachname"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:8
|
||||
#: lib/mv_web/member_live/index.ex:88
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Listing Members"
|
||||
msgstr "Mitglieder"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:11
|
||||
#: lib/mv_web/member_live/index.ex:82
|
||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New Member"
|
||||
msgstr "Neues Mitglied"
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:30
|
||||
#: lib/mv_web/live/member_live/index.html.heex:68
|
||||
#: lib/mv_web/live/user_live/index.html.heex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr "Anzeigen"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:167
|
||||
#: lib/mv_web/components/layouts.ex:78
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Something went wrong!"
|
||||
msgstr "Etwas ist schiefgelaufen!"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:150
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Success!"
|
||||
msgstr "Erfolg!"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:155
|
||||
#: lib/mv_web/components/layouts.ex:66
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "We can't find the internet"
|
||||
msgstr "Keine Internetverbindung gefunden"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:76
|
||||
#: lib/mv_web/components/core_components.ex:130
|
||||
#: lib/mv_web/components/core_components.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "close"
|
||||
msgstr "schließen"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:44
|
||||
#: lib/mv_web/member_live/show.ex:24
|
||||
#: lib/mv_web/live/member_live/form.ex:19
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Birth Date"
|
||||
msgstr "Geburtsdatum"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:55
|
||||
#: lib/mv_web/member_live/show.ex:38
|
||||
#: lib/mv_web/live/member_live/form.ex:30
|
||||
#: lib/mv_web/live/member_live/show.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom Properties"
|
||||
msgstr "Eigene Eigenschaften"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:48
|
||||
#: lib/mv_web/member_live/show.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:23
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Exit Date"
|
||||
msgstr "Austrittsdatum"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:52
|
||||
#: lib/mv_web/member_live/show.ex:34
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
msgstr "Hausnummer"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:49
|
||||
#: lib/mv_web/member_live/show.ex:31
|
||||
#: lib/mv_web/live/member_live/form.ex:24
|
||||
#: lib/mv_web/live/member_live/show.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Notes"
|
||||
msgstr "Notizen"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:45
|
||||
#: lib/mv_web/member_live/show.ex:25
|
||||
#: lib/mv_web/live/member_live/form.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Paid"
|
||||
msgstr "Bezahlt"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:46
|
||||
#: lib/mv_web/member_live/show.ex:28
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/show.ex:32
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr "Telefonnummer"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:53
|
||||
#: lib/mv_web/member_live/show.ex:35
|
||||
#: lib/mv_web/live/member_live/form.ex:28
|
||||
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
msgstr "Postleitzahl"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Member"
|
||||
msgstr "Mitglied speichern"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:49
|
||||
#: lib/mv_web/live/property_live/form.ex:41
|
||||
#: lib/mv_web/live/property_type_live/form.ex:29
|
||||
#: lib/mv_web/live/user_live/form.ex:92
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Saving..."
|
||||
msgstr "Speichern..."
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:51
|
||||
#: lib/mv_web/member_live/show.ex:33
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:59
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
msgstr "Straße"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage member records and their properties."
|
||||
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:52
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Back to members"
|
||||
msgstr "Zurück zur Mitgliederliste"
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit member"
|
||||
msgstr "Mitglied bearbeiten"
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
msgstr "Nein"
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:92
|
||||
#: lib/mv_web/live/member_live/show.ex:80
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Show Member"
|
||||
msgstr "Mitglied anzeigen"
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:10
|
||||
#: lib/mv_web/live/member_live/show.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This is a member record from your database."
|
||||
msgstr "Dies ist ein Mitglied aus deiner Datenbank."
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr "Ja"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:102
|
||||
#: lib/mv_web/live/member_live/form.ex:108
|
||||
#: lib/mv_web/live/property_live/form.ex:200
|
||||
#: lib/mv_web/live/property_type_live/form.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr "erstellt"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:103
|
||||
#: lib/mv_web/live/member_live/form.ex:109
|
||||
#: lib/mv_web/live/property_live/form.ex:201
|
||||
#: lib/mv_web/live/property_type_live/form.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "update"
|
||||
msgstr "aktualisiert"
|
||||
|
|
@ -241,9 +226,9 @@ msgstr "aktualisiert"
|
|||
#: lib/mv_web/controllers/auth_controller.ex:43
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Incorrect email or password"
|
||||
msgstr ""
|
||||
msgstr "Falsche E-Mail oder Passwort"
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:109
|
||||
#: lib/mv_web/live/member_live/form.ex:115
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member %{action} successfully"
|
||||
msgstr "Mitglied %{action} erfolgreich"
|
||||
|
|
@ -251,24 +236,319 @@ msgstr "Mitglied %{action} erfolgreich"
|
|||
#: lib/mv_web/controllers/auth_controller.ex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You are now signed in"
|
||||
msgstr ""
|
||||
msgstr "Sie sind jetzt angemeldet"
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You are now signed out"
|
||||
msgstr ""
|
||||
msgstr "Sie sind jetzt abgemeldet"
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:36
|
||||
#: lib/mv_web/controllers/auth_controller.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n"
|
||||
msgstr ""
|
||||
msgstr "Sie haben sich bereits auf andere Weise angemeldet, aber Ihr Konto noch nicht bestätigt.\nSie können Ihr Konto über den Link bestätigen, den wir Ihnen gesendet haben, oder durch Zurücksetzen Ihres Passworts.\n"
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:12
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Your email address has now been confirmed"
|
||||
msgstr ""
|
||||
msgstr "Ihre E-Mail-Adresse wurde bestätigt"
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:13
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:52
|
||||
#: lib/mv_web/live/property_live/form.ex:44
|
||||
#: lib/mv_web/live/property_type_live/form.ex:32
|
||||
#: lib/mv_web/live/user_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a member"
|
||||
msgstr "Mitglied auswählen"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a property type"
|
||||
msgstr "Eigenschaftstyp auswählen"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:17
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit User"
|
||||
msgstr "Benutzer bearbeiten"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Enabled"
|
||||
msgstr "Aktiviert"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr "Unveränderlich"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Logout"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.ex:12
|
||||
#: lib/mv_web/live/user_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Listing Users"
|
||||
msgstr "Benutzer auflisten"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: lib/mv_web/live/member_live/index.ex:12
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr "Mitglieder"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:50
|
||||
#: lib/mv_web/live/property_type_live/form.ex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New User"
|
||||
msgstr "Neuer Benutzer"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Not enabled"
|
||||
msgstr "Nicht aktiviert"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Not set"
|
||||
msgstr "Nicht gesetzt"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Note"
|
||||
msgstr "Hinweis"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "OIDC ID"
|
||||
msgstr "OIDC ID"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password Authentication"
|
||||
msgstr "Passwort-Authentifizierung"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Please select a property type first"
|
||||
msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Profil"
|
||||
msgstr "Profil"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:207
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Property %{action} successfully"
|
||||
msgstr "Mitglied %{action} erfolgreich"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:18
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type"
|
||||
msgstr "Eigenschaftstyp"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:80
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type %{action} successfully"
|
||||
msgstr "Eigenschaftstyp %{action} erfolgreich"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr "Erforderlich"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property"
|
||||
msgstr "Eigenschaft speichern"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property type"
|
||||
msgstr "Eigenschaftstyp speichern"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr "Alle Mitglieder auswählen"
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr "Mitglied auswählen"
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:72
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:93
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save User"
|
||||
msgstr "Benutzer speichern"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show User"
|
||||
msgstr "Benutzer anzeigen"
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This is a user record from your database."
|
||||
msgstr "Dies ist ein Benutzer-Datensatz aus Ihrer Datenbank."
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unsupported value type: %{type}"
|
||||
msgstr "Nicht unterstützter Wertetyp: %{type}"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage property records in your database."
|
||||
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage property_type records in your database."
|
||||
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage user records in your database."
|
||||
msgstr "Verwenden Sie dieses Formular, um Benutzer-Datensätze zu verwalten."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:110
|
||||
#: lib/mv_web/live/user_live/show.ex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value"
|
||||
msgstr "Wert"
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr "Wertetyp"
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ascending"
|
||||
msgstr "aufsteigend"
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "descending"
|
||||
msgstr "absteigend"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New"
|
||||
msgstr "Neuer"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Admin Note"
|
||||
msgstr "Administrator-Hinweis"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
|
||||
msgstr "Als Administrator können Sie direkt ein neues Passwort für diesen Benutzer setzen, wobei das gleiche sichere Ash Authentication System verwendet wird."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "At least 8 characters"
|
||||
msgstr "Mindestens 8 Zeichen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Change Password"
|
||||
msgstr "Passwort ändern"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Check 'Change Password' above to set a new password for this user."
|
||||
msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diesen Benutzer zu setzen."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:45
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm Password"
|
||||
msgstr "Passwort bestätigen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:57
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Consider using special characters"
|
||||
msgstr "Sonderzeichen empfohlen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Include both letters and numbers"
|
||||
msgstr "Buchstaben und Zahlen verwenden"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password requirements"
|
||||
msgstr "Passwort-Anforderungen"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all users"
|
||||
msgstr "Alle Benutzer auswählen"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select user"
|
||||
msgstr "Benutzer auswählen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set Password"
|
||||
msgstr "Passwort setzen"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User will be created without a password. Check 'Set Password' to add one."
|
||||
msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
|
||||
|
|
|
|||
|
|
@ -11,230 +11,215 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:482
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/user_live/index.html.heex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:160
|
||||
#: lib/mv_web/components/layouts.ex:71
|
||||
#: lib/mv_web/components/layouts.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Attempting to reconnect"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:50
|
||||
#: lib/mv_web/member_live/index.ex:25
|
||||
#: lib/mv_web/member_live/show.ex:32
|
||||
#: lib/mv_web/live/member_live/form.ex:25
|
||||
#: lib/mv_web/live/member_live/index.html.heex:62
|
||||
#: lib/mv_web/live/member_live/show.ex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:41
|
||||
#: lib/mv_web/live/member_live/index.html.heex:79
|
||||
#: lib/mv_web/live/user_live/index.html.heex:71
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:33
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:76
|
||||
#: lib/mv_web/member_live/show.ex:93
|
||||
#: lib/mv_web/live/member_live/show.ex:18
|
||||
#: lib/mv_web/live/member_live/show.ex:81
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:43
|
||||
#: lib/mv_web/member_live/index.ex:24
|
||||
#: lib/mv_web/member_live/show.ex:23
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:58
|
||||
#: lib/mv_web/live/member_live/show.ex:27
|
||||
#: lib/mv_web/live/user_live/form.ex:14
|
||||
#: lib/mv_web/live/user_live/index.html.heex:48
|
||||
#: lib/mv_web/live/user_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:151
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Error!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:41
|
||||
#: lib/mv_web/member_live/index.ex:22
|
||||
#: lib/mv_web/member_live/show.ex:21
|
||||
#: lib/mv_web/live/member_live/form.ex:16
|
||||
#: lib/mv_web/live/member_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:172
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Hang in there while we get back on track"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:47
|
||||
#: lib/mv_web/member_live/index.ex:26
|
||||
#: lib/mv_web/member_live/show.ex:29
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:42
|
||||
#: lib/mv_web/member_live/index.ex:23
|
||||
#: lib/mv_web/member_live/show.ex:22
|
||||
#: lib/mv_web/live/member_live/form.ex:17
|
||||
#: lib/mv_web/live/member_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:8
|
||||
#: lib/mv_web/member_live/index.ex:88
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Listing Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:11
|
||||
#: lib/mv_web/member_live/index.ex:82
|
||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:30
|
||||
#: lib/mv_web/live/member_live/index.html.heex:68
|
||||
#: lib/mv_web/live/user_live/index.html.heex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:167
|
||||
#: lib/mv_web/components/layouts.ex:78
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Something went wrong!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:150
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Success!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:155
|
||||
#: lib/mv_web/components/layouts.ex:66
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "We can't find the internet"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:76
|
||||
#: lib/mv_web/components/core_components.ex:130
|
||||
#: lib/mv_web/components/core_components.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:44
|
||||
#: lib/mv_web/member_live/show.ex:24
|
||||
#: lib/mv_web/live/member_live/form.ex:19
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Birth Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:55
|
||||
#: lib/mv_web/member_live/show.ex:38
|
||||
#: lib/mv_web/live/member_live/form.ex:30
|
||||
#: lib/mv_web/live/member_live/show.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom Properties"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:48
|
||||
#: lib/mv_web/member_live/show.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:23
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Exit Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:52
|
||||
#: lib/mv_web/member_live/show.ex:34
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:49
|
||||
#: lib/mv_web/member_live/show.ex:31
|
||||
#: lib/mv_web/live/member_live/form.ex:24
|
||||
#: lib/mv_web/live/member_live/show.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:45
|
||||
#: lib/mv_web/member_live/show.ex:25
|
||||
#: lib/mv_web/live/member_live/form.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:46
|
||||
#: lib/mv_web/member_live/show.ex:28
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/show.ex:32
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:53
|
||||
#: lib/mv_web/member_live/show.ex:35
|
||||
#: lib/mv_web/live/member_live/form.ex:28
|
||||
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:49
|
||||
#: lib/mv_web/live/property_live/form.ex:41
|
||||
#: lib/mv_web/live/property_type_live/form.ex:29
|
||||
#: lib/mv_web/live/user_live/form.ex:92
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Saving..."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:51
|
||||
#: lib/mv_web/member_live/show.ex:33
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:59
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage member records and their properties."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:52
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Back to members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:14
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:92
|
||||
#: lib/mv_web/live/member_live/show.ex:80
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:10
|
||||
#: lib/mv_web/live/member_live/show.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This is a member record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:102
|
||||
#: lib/mv_web/live/member_live/form.ex:108
|
||||
#: lib/mv_web/live/property_live/form.ex:200
|
||||
#: lib/mv_web/live/property_type_live/form.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:103
|
||||
#: lib/mv_web/live/member_live/form.ex:109
|
||||
#: lib/mv_web/live/property_live/form.ex:201
|
||||
#: lib/mv_web/live/property_type_live/form.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "update"
|
||||
msgstr ""
|
||||
|
|
@ -244,7 +229,7 @@ msgstr ""
|
|||
msgid "Incorrect email or password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:109
|
||||
#: lib/mv_web/live/member_live/form.ex:115
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member %{action} successfully"
|
||||
msgstr ""
|
||||
|
|
@ -259,7 +244,7 @@ msgstr ""
|
|||
msgid "You are now signed out"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:36
|
||||
#: lib/mv_web/controllers/auth_controller.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n"
|
||||
msgstr ""
|
||||
|
|
@ -273,3 +258,298 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:52
|
||||
#: lib/mv_web/live/property_live/form.ex:44
|
||||
#: lib/mv_web/live/property_type_live/form.ex:32
|
||||
#: lib/mv_web/live/user_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:17
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.ex:12
|
||||
#: lib/mv_web/live/user_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Listing Users"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: lib/mv_web/live/member_live/index.ex:12
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:50
|
||||
#: lib/mv_web/live/property_type_live/form.ex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Not enabled"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "OIDC ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Please select a property type first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Profil"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:207
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property %{action} successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:18
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:80
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type %{action} successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:72
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:93
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This is a user record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unsupported value type: %{type}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage property records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage property_type records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage user records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:110
|
||||
#: lib/mv_web/live/user_live/show.ex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ascending"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "descending"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Admin Note"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "At least 8 characters"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Change Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Check 'Change Password' above to set a new password for this user."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:45
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:57
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Consider using special characters"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Include both letters and numbers"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password requirements"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all users"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User will be created without a password. Check 'Set Password' to add one."
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -11,230 +11,215 @@ msgstr ""
|
|||
"Language: en\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:482
|
||||
#: lib/mv_web/components/core_components.ex:339
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:39
|
||||
#: lib/mv_web/live/member_live/index.html.heex:77
|
||||
#: lib/mv_web/live/user_live/index.html.heex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:160
|
||||
#: lib/mv_web/components/layouts.ex:71
|
||||
#: lib/mv_web/components/layouts.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Attempting to reconnect"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:50
|
||||
#: lib/mv_web/member_live/index.ex:25
|
||||
#: lib/mv_web/member_live/show.ex:32
|
||||
#: lib/mv_web/live/member_live/form.ex:25
|
||||
#: lib/mv_web/live/member_live/index.html.heex:62
|
||||
#: lib/mv_web/live/member_live/show.ex:36
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "City"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:41
|
||||
#: lib/mv_web/live/member_live/index.html.heex:79
|
||||
#: lib/mv_web/live/user_live/index.html.heex:71
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:33
|
||||
#: lib/mv_web/live/member_live/index.html.heex:71
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#: lib/mv_web/live/user_live/index.html.heex:63
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:76
|
||||
#: lib/mv_web/member_live/show.ex:93
|
||||
#: lib/mv_web/live/member_live/show.ex:18
|
||||
#: lib/mv_web/live/member_live/show.ex:81
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Edit Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:43
|
||||
#: lib/mv_web/member_live/index.ex:24
|
||||
#: lib/mv_web/member_live/show.ex:23
|
||||
#: lib/mv_web/live/member_live/form.ex:18
|
||||
#: lib/mv_web/live/member_live/index.html.heex:58
|
||||
#: lib/mv_web/live/member_live/show.ex:27
|
||||
#: lib/mv_web/live/user_live/form.ex:14
|
||||
#: lib/mv_web/live/user_live/index.html.heex:48
|
||||
#: lib/mv_web/live/user_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:151
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Error!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:41
|
||||
#: lib/mv_web/member_live/index.ex:22
|
||||
#: lib/mv_web/member_live/show.ex:21
|
||||
#: lib/mv_web/live/member_live/form.ex:16
|
||||
#: lib/mv_web/live/member_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "First Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:172
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Hang in there while we get back on track"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:47
|
||||
#: lib/mv_web/member_live/index.ex:26
|
||||
#: lib/mv_web/member_live/show.ex:29
|
||||
#: lib/mv_web/live/member_live/form.ex:22
|
||||
#: lib/mv_web/live/member_live/index.html.heex:64
|
||||
#: lib/mv_web/live/member_live/show.ex:33
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Join Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:42
|
||||
#: lib/mv_web/member_live/index.ex:23
|
||||
#: lib/mv_web/member_live/show.ex:22
|
||||
#: lib/mv_web/live/member_live/form.ex:17
|
||||
#: lib/mv_web/live/member_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Last Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:8
|
||||
#: lib/mv_web/member_live/index.ex:88
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Listing Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:11
|
||||
#: lib/mv_web/member_live/index.ex:82
|
||||
#: lib/mv_web/live/member_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/index.ex:30
|
||||
#: lib/mv_web/live/member_live/index.html.heex:68
|
||||
#: lib/mv_web/live/user_live/index.html.heex:60
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Show"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:167
|
||||
#: lib/mv_web/components/layouts.ex:78
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Something went wrong!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:150
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Success!"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:155
|
||||
#: lib/mv_web/components/layouts.ex:66
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "We can't find the internet"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/core_components.ex:76
|
||||
#: lib/mv_web/components/core_components.ex:130
|
||||
#: lib/mv_web/components/core_components.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "close"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:44
|
||||
#: lib/mv_web/member_live/show.ex:24
|
||||
#: lib/mv_web/live/member_live/form.ex:19
|
||||
#: lib/mv_web/live/member_live/show.ex:28
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Birth Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:55
|
||||
#: lib/mv_web/member_live/show.ex:38
|
||||
#: lib/mv_web/live/member_live/form.ex:30
|
||||
#: lib/mv_web/live/member_live/show.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Custom Properties"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:48
|
||||
#: lib/mv_web/member_live/show.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:23
|
||||
#: lib/mv_web/live/member_live/show.ex:34
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Exit Date"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:52
|
||||
#: lib/mv_web/member_live/show.ex:34
|
||||
#: lib/mv_web/live/member_live/form.ex:27
|
||||
#: lib/mv_web/live/member_live/index.html.heex:60
|
||||
#: lib/mv_web/live/member_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "House Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:49
|
||||
#: lib/mv_web/member_live/show.ex:31
|
||||
#: lib/mv_web/live/member_live/form.ex:24
|
||||
#: lib/mv_web/live/member_live/show.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:45
|
||||
#: lib/mv_web/member_live/show.ex:25
|
||||
#: lib/mv_web/live/member_live/form.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Paid"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:46
|
||||
#: lib/mv_web/member_live/show.ex:28
|
||||
#: lib/mv_web/live/member_live/form.ex:21
|
||||
#: lib/mv_web/live/member_live/index.html.heex:63
|
||||
#: lib/mv_web/live/member_live/show.ex:32
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Phone Number"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:53
|
||||
#: lib/mv_web/member_live/show.ex:35
|
||||
#: lib/mv_web/live/member_live/form.ex:28
|
||||
#: lib/mv_web/live/member_live/index.html.heex:61
|
||||
#: lib/mv_web/live/member_live/show.ex:39
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Postal Code"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:50
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Save Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:75
|
||||
#: lib/mv_web/live/member_live/form.ex:49
|
||||
#: lib/mv_web/live/property_live/form.ex:41
|
||||
#: lib/mv_web/live/property_type_live/form.ex:29
|
||||
#: lib/mv_web/live/user_live/form.ex:92
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Saving..."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:51
|
||||
#: lib/mv_web/member_live/show.ex:33
|
||||
#: lib/mv_web/live/member_live/form.ex:26
|
||||
#: lib/mv_web/live/member_live/index.html.heex:59
|
||||
#: lib/mv_web/live/member_live/show.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Street"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:30
|
||||
#: lib/mv_web/live/member_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Use this form to manage member records and their properties."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:52
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Back to members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:14
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Edit member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:20
|
||||
#: lib/mv_web/live/member_live/show.ex:24
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Id"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:92
|
||||
#: lib/mv_web/live/member_live/show.ex:80
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Show Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:10
|
||||
#: lib/mv_web/live/member_live/show.ex:11
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "This is a member record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/show.ex:26
|
||||
#: lib/mv_web/live/member_live/show.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:102
|
||||
#: lib/mv_web/live/member_live/form.ex:108
|
||||
#: lib/mv_web/live/property_live/form.ex:200
|
||||
#: lib/mv_web/live/property_type_live/form.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "create"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:103
|
||||
#: lib/mv_web/live/member_live/form.ex:109
|
||||
#: lib/mv_web/live/property_live/form.ex:201
|
||||
#: lib/mv_web/live/property_type_live/form.ex:74
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "update"
|
||||
msgstr ""
|
||||
|
|
@ -244,7 +229,7 @@ msgstr ""
|
|||
msgid "Incorrect email or password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/member_live/form_component.ex:109
|
||||
#: lib/mv_web/live/member_live/form.ex:115
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member %{action} successfully"
|
||||
msgstr ""
|
||||
|
|
@ -259,7 +244,7 @@ msgstr ""
|
|||
msgid "You are now signed out"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/controllers/auth_controller.ex:36
|
||||
#: lib/mv_web/controllers/auth_controller.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n"
|
||||
msgstr ""
|
||||
|
|
@ -273,3 +258,298 @@ msgstr ""
|
|||
#, elixir-autogen, elixir-format
|
||||
msgid "Your password has successfully been reset"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/form.ex:52
|
||||
#: lib/mv_web/live/property_live/form.ex:44
|
||||
#: lib/mv_web/live/property_type_live/form.ex:32
|
||||
#: lib/mv_web/live/user_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:29
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Choose a property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:17
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Edit User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:23
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Immutable"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:73
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.ex:12
|
||||
#: lib/mv_web/live/user_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Listing Users"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:14
|
||||
#: lib/mv_web/live/member_live/index.ex:12
|
||||
#: lib/mv_web/live/member_live/index.html.heex:3
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:50
|
||||
#: lib/mv_web/live/property_type_live/form.ex:16
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:6
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Not enabled"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:56
|
||||
#: lib/mv_web/live/user_live/show.ex:25
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "OIDC ID"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:26
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password Authentication"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:37
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Please select a property type first"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:69
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Profil"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:207
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Property %{action} successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:18
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:80
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Property type %{action} successfully"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:42
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Save Property type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select all members"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/member_live/index.html.heex:41
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Select member"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/layouts/navbar.ex:72
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:93
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Save User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:38
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Show User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/show.ex:10
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "This is a user record from your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:95
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Unsupported value type: %{type}"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage property records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:11
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage property_type records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:10
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Use this form to manage user records in your database."
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:110
|
||||
#: lib/mv_web/live/user_live/show.ex:9
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_live/form.ex:59
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/property_type_live/form.ex:20
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Value type"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "ascending"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/components/table_components.ex:30
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "descending"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:109
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Admin Note"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:64
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
|
||||
msgstr "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:55
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "At least 8 characters"
|
||||
msgstr "At least 8 characters"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Change Password"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:75
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Check 'Change Password' above to set a new password for this user."
|
||||
msgstr "Check 'Change Password' above to set a new password for this user."
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:45
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Confirm Password"
|
||||
msgstr "Confirm Password"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:57
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Consider using special characters"
|
||||
msgstr "Consider using special characters"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:56
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Include both letters and numbers"
|
||||
msgstr "Include both letters and numbers"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:35
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:53
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Password requirements"
|
||||
msgstr "Password requirements"
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:25
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Select all users"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/index.html.heex:39
|
||||
#, elixir-autogen, elixir-format, fuzzy
|
||||
msgid "Select user"
|
||||
msgstr ""
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:27
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "Set Password"
|
||||
msgstr "Set Password"
|
||||
|
||||
#: lib/mv_web/live/user_live/form.ex:83
|
||||
#, elixir-autogen, elixir-format
|
||||
msgid "User will be created without a password. Check 'Set Password' to add one."
|
||||
msgstr "User will be created without a password. Check 'Set Password' to add one."
|
||||
|
|
|
|||
|
|
@ -1,40 +1,121 @@
|
|||
defmodule MvWeb.AuthControllerTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
# Basic UI tests
|
||||
test "GET /sign-in shows sign in form", %{conn: conn} do
|
||||
conn = get(conn, ~p"/sign-in")
|
||||
assert html_response(conn, 200) =~ "Sign in"
|
||||
end
|
||||
|
||||
test "POST /sign-in with valid credentials redirects to home", %{conn: conn} do
|
||||
# Create a test user first
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = get(conn, ~p"/sign-in")
|
||||
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
|
||||
test "POST /sign-in with invalid credentials shows error", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, ~p"/auth/sign_in", %{
|
||||
"user" => %{
|
||||
"email" => "wrong@example.com",
|
||||
"password" => "wrongpassword"
|
||||
}
|
||||
})
|
||||
|
||||
assert conn.status == 404
|
||||
end
|
||||
|
||||
test "GET /sign-out redirects to home", %{conn: conn} do
|
||||
# First sign in a user
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Then sign out
|
||||
conn = get(conn, ~p"/sign-out")
|
||||
assert redirected_to(conn) == ~p"/"
|
||||
end
|
||||
|
||||
# Password authentication (LiveView)
|
||||
test "password user can sign in with valid credentials via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "password@example.com",
|
||||
password: "secret123",
|
||||
oidc_id: nil
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
{:error, {:redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "password@example.com", password: "secret123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert to =~ "/auth/user/password/sign_in_with_token"
|
||||
end
|
||||
|
||||
test "password user with invalid credentials shows error via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "test@example.com",
|
||||
password: "correct_password",
|
||||
oidc_id: nil
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "test@example.com", password: "wrong_password"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Email or password was incorrect"
|
||||
end
|
||||
|
||||
test "password user with non-existent email shows error via LiveView", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "nonexistent@example.com", password: "anypassword"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Email or password was incorrect"
|
||||
end
|
||||
|
||||
# Registration (LiveView)
|
||||
test "user can register with valid credentials via LiveView", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/register")
|
||||
|
||||
{:error, {:redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("#user-password-register-with-password-wrapper form",
|
||||
user: %{email: "newuser@example.com", password: "newpassword123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert to =~ "/auth/user/password/sign_in_with_token"
|
||||
end
|
||||
|
||||
test "registration with existing email shows error via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "existing@example.com",
|
||||
password: "secret123",
|
||||
oidc_id: nil
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/register")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-password-register-with-password-wrapper form",
|
||||
user: %{email: "existing@example.com", password: "anotherpassword"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "registration with weak password shows error via LiveView", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/register")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-password-register-with-password-wrapper form",
|
||||
user: %{email: "weakpass@example.com", password: "123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "length must be greater than or equal to 8"
|
||||
end
|
||||
|
||||
# Access control
|
||||
test "unauthenticated user accessing protected route gets redirected to sign-in", %{conn: conn} do
|
||||
conn = get(conn, ~p"/members")
|
||||
assert redirected_to(conn) == ~p"/sign-in"
|
||||
|
|
@ -45,4 +126,67 @@ defmodule MvWeb.AuthControllerTest do
|
|||
conn = get(conn, ~p"/members")
|
||||
assert conn.status == 200
|
||||
end
|
||||
|
||||
test "password authenticated user can access protected route via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "auth@example.com",
|
||||
password: "secret123",
|
||||
oidc_id: nil
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
{:error, {:redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "auth@example.com", password: "secret123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert to =~ "/auth/user/password/sign_in_with_token"
|
||||
|
||||
# After login, user is redirected to /auth/user/password/sign_in_with_token. Session handling for protected routes should be tested in integration or E2E tests.
|
||||
end
|
||||
|
||||
# Edge cases
|
||||
test "user with nil oidc_id can still sign in with password via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "nil_oidc@example.com",
|
||||
password: "secret123",
|
||||
oidc_id: nil
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
{:error, {:redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "nil_oidc@example.com", password: "secret123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert to =~ "/auth/user/password/sign_in_with_token"
|
||||
end
|
||||
|
||||
test "user with empty string oidc_id is handled correctly via LiveView", %{conn: conn} do
|
||||
_user =
|
||||
create_test_user(%{
|
||||
email: "empty_oidc@example.com",
|
||||
password: "secret123",
|
||||
oidc_id: ""
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/sign-in")
|
||||
|
||||
{:error, {:redirect, %{to: to}}} =
|
||||
view
|
||||
|> form("#user-password-sign-in-with-password",
|
||||
user: %{email: "empty_oidc@example.com", password: "secret123"}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert to =~ "/auth/user/password/sign_in_with_token"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ defmodule MvWeb.ErrorHTMLTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
|
||||
# Bring render_to_string/4 for testing custom views
|
||||
import Phoenix.Template
|
||||
import Phoenix.Template, only: [render_to_string: 4]
|
||||
|
||||
test "renders 404.html" do
|
||||
assert render_to_string(MvWeb.ErrorHTML, "404", "html", []) == "Not Found"
|
||||
|
|
|
|||
166
test/mv_web/controllers/oidc_integration_test.exs
Normal file
166
test/mv_web/controllers/oidc_integration_test.exs
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
defmodule MvWeb.OidcIntegrationTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
# Test OIDC callback scenarios by directly calling the actions
|
||||
# This simulates what happens during real OIDC authentication
|
||||
|
||||
describe "OIDC sign-in scenarios" do
|
||||
test "existing OIDC user with unchanged email can sign in" do
|
||||
# Create user with OIDC ID
|
||||
user =
|
||||
create_test_user(%{
|
||||
email: "existing@example.com",
|
||||
oidc_id: "existing_oidc_123"
|
||||
})
|
||||
|
||||
# Simulate OIDC callback data
|
||||
user_info = %{
|
||||
"sub" => "existing_oidc_123",
|
||||
"preferred_username" => "existing@example.com"
|
||||
}
|
||||
|
||||
# Test sign_in_with_rauthy action directly
|
||||
{:ok, [found_user]} =
|
||||
Mv.Accounts.read_sign_in_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
assert found_user.id == user.id
|
||||
assert to_string(found_user.email) == "existing@example.com"
|
||||
assert found_user.oidc_id == "existing_oidc_123"
|
||||
end
|
||||
|
||||
test "new OIDC user gets created via register_with_rauthy" do
|
||||
# Simulate OIDC callback for completely new user
|
||||
user_info = %{
|
||||
"sub" => "brand_new_oidc_456",
|
||||
"preferred_username" => "newuser@example.com"
|
||||
}
|
||||
|
||||
# Test register_with_rauthy action
|
||||
case Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
}) do
|
||||
{:ok, new_user} ->
|
||||
assert to_string(new_user.email) == "newuser@example.com"
|
||||
assert new_user.oidc_id == "brand_new_oidc_456"
|
||||
assert is_nil(new_user.hashed_password)
|
||||
|
||||
{:error, error} ->
|
||||
flunk("Should have created new user: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "OIDC error and edge case scenarios" do
|
||||
test "OIDC registration with conflicting email and OIDC ID shows error" do
|
||||
# Create user with email and OIDC ID
|
||||
_existing_user =
|
||||
create_test_user(%{
|
||||
email: "conflict@example.com",
|
||||
oidc_id: "oidc_conflict_1"
|
||||
})
|
||||
|
||||
# Try to register with same email but different OIDC ID
|
||||
user_info = %{
|
||||
"sub" => "oidc_conflict_2",
|
||||
"preferred_username" => "conflict@example.com"
|
||||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
# Should fail due to unique constraint
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
||||
assert Enum.any?(errors, fn
|
||||
%Ash.Error.Changes.InvalidAttribute{field: :email, message: message} ->
|
||||
String.contains?(message, "has already been taken")
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
test "OIDC registration with missing sub and id should fail" do
|
||||
user_info = %{
|
||||
"preferred_username" => "nosub@example.com"
|
||||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
assert {:error,
|
||||
%Ash.Error.Invalid{
|
||||
errors: [%Ash.Error.Changes.InvalidChanges{vars: [user_info: msg]}]
|
||||
}} = result
|
||||
|
||||
assert String.contains?(msg, "OIDC user_info must contain a non-empty 'sub' or 'id' field")
|
||||
end
|
||||
|
||||
test "OIDC registration with missing preferred_username should fail" do
|
||||
user_info = %{
|
||||
"sub" => "noemail_oidc_123"
|
||||
}
|
||||
|
||||
result =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = result
|
||||
|
||||
assert Enum.any?(errors, fn err ->
|
||||
match?(%Ash.Error.Changes.Required{field: :email}, err)
|
||||
end)
|
||||
end
|
||||
|
||||
test "OIDC registration with existing OIDC ID and different email updates email" do
|
||||
existing_user =
|
||||
create_test_user(%{
|
||||
email: "old@example.com",
|
||||
oidc_id: "oidc_update_email"
|
||||
})
|
||||
|
||||
user_info = %{
|
||||
"sub" => "oidc_update_email",
|
||||
"preferred_username" => "new@example.com"
|
||||
}
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
assert user.id == existing_user.id
|
||||
assert to_string(user.email) == "new@example.com"
|
||||
assert user.oidc_id == "oidc_update_email"
|
||||
end
|
||||
|
||||
test "OIDC registration with alternative OIDC ID field (id instead of sub)" do
|
||||
user_info = %{
|
||||
"id" => "alt_oidc_id_123",
|
||||
"preferred_username" => "altid@example.com"
|
||||
}
|
||||
|
||||
{:ok, user} =
|
||||
Mv.Accounts.create_register_with_rauthy(%{
|
||||
user_info: user_info,
|
||||
oauth_tokens: %{}
|
||||
})
|
||||
|
||||
assert user.oidc_id == "alt_oidc_id_123"
|
||||
assert to_string(user.email) == "altid@example.com"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -35,8 +35,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
test "shows translated flash message after creating a member in German", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
view |> element("a", "Neues Mitglied") |> render_click()
|
||||
{:ok, form_view, _html} = live(conn, "/members/new")
|
||||
|
||||
form_data = %{
|
||||
"member[first_name]" => "Max",
|
||||
|
|
@ -44,15 +43,20 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
"member[email]" => "max@example.com"
|
||||
}
|
||||
|
||||
view |> form("#member-form", form_data) |> render_submit()
|
||||
assert has_element?(view, "#flash-group", "Mitglied erstellt erfolgreich")
|
||||
# Submit form and follow the redirect to get the flash message
|
||||
{:ok, index_view, _html} =
|
||||
form_view
|
||||
|> form("#member-form", form_data)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, "/members")
|
||||
|
||||
assert has_element?(index_view, "#flash-group", "Mitglied erstellt erfolgreich")
|
||||
end
|
||||
|
||||
test "shows translated flash message after creating a member in English", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = Plug.Test.init_test_session(conn, locale: "en")
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
view |> element("a", "New Member") |> render_click()
|
||||
{:ok, form_view, _html} = live(conn, "/members/new")
|
||||
|
||||
form_data = %{
|
||||
"member[first_name]" => "Max",
|
||||
|
|
@ -60,7 +64,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
"member[email]" => "max@example.com"
|
||||
}
|
||||
|
||||
view |> form("#member-form", form_data) |> render_submit()
|
||||
assert has_element?(view, "#flash-group", "Member create successfully")
|
||||
# Submit form and follow the redirect to get the flash message
|
||||
{:ok, index_view, _html} =
|
||||
form_view
|
||||
|> form("#member-form", form_data)
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn, "/members")
|
||||
|
||||
assert has_element?(index_view, "#flash-group", "Member create successfully")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
284
test/mv_web/user_live/form_test.exs
Normal file
284
test/mv_web/user_live/form_test.exs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
defmodule MvWeb.UserLive.FormTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
# Helper to setup authenticated connection and live view
|
||||
defp setup_live_view(conn, path) do
|
||||
conn = conn_with_oidc_user(conn, %{email: "admin@example.com"})
|
||||
live(conn, path)
|
||||
end
|
||||
|
||||
describe "new user form - display" do
|
||||
test "shows correct form elements", %{conn: conn} do
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
assert html =~ "New User"
|
||||
assert html =~ "Email"
|
||||
assert html =~ "Set Password"
|
||||
assert has_element?(view, "form#user-form[phx-submit='save']")
|
||||
assert has_element?(view, "input[name='user[email]']")
|
||||
assert has_element?(view, "input[type='checkbox'][name='set_password']")
|
||||
end
|
||||
|
||||
test "hides password fields initially", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
refute has_element?(view, "input[name='user[password]']")
|
||||
refute has_element?(view, "input[name='user[password_confirmation]']")
|
||||
end
|
||||
|
||||
test "shows password fields when checkbox toggled", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
assert has_element?(view, "input[name='user[password]']")
|
||||
assert has_element?(view, "input[name='user[password_confirmation]']")
|
||||
assert render(view) =~ "Password requirements"
|
||||
end
|
||||
end
|
||||
|
||||
describe "new user form - creation" do
|
||||
test "creates user without password", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view
|
||||
|> form("#user-form", user: %{email: "newuser@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
end
|
||||
|
||||
test "creates user with password when enabled", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
view
|
||||
|> form("#user-form",
|
||||
user: %{
|
||||
email: "passworduser@example.com",
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
end
|
||||
|
||||
test "stores user data correctly", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view
|
||||
|> form("#user-form", user: %{email: "storetest@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
user =
|
||||
Ash.get!(
|
||||
Mv.Accounts.User,
|
||||
[email: Ash.CiString.new("storetest@example.com")],
|
||||
domain: Mv.Accounts
|
||||
)
|
||||
|
||||
assert to_string(user.email) == "storetest@example.com"
|
||||
assert is_nil(user.hashed_password)
|
||||
end
|
||||
|
||||
test "stores password when provided", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
view
|
||||
|> form("#user-form",
|
||||
user: %{
|
||||
email: "passwordstoretest@example.com",
|
||||
password: "securepassword123",
|
||||
password_confirmation: "securepassword123"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
user =
|
||||
Ash.get!(
|
||||
Mv.Accounts.User,
|
||||
[email: Ash.CiString.new("passwordstoretest@example.com")],
|
||||
domain: Mv.Accounts
|
||||
)
|
||||
|
||||
assert user.hashed_password != nil
|
||||
assert String.starts_with?(user.hashed_password, "$2b$")
|
||||
end
|
||||
end
|
||||
|
||||
describe "new user form - validation" do
|
||||
test "shows error for duplicate email", %{conn: conn} do
|
||||
_existing_user = create_test_user(%{email: "existing@example.com"})
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-form", user: %{email: "existing@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "shows error for short password", %{conn: conn} do
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/new")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-form",
|
||||
user: %{
|
||||
email: "test@example.com",
|
||||
password: "123",
|
||||
password_confirmation: "123"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "length must be greater than or equal to 8"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit user form - display" do
|
||||
test "shows correct form elements for existing user", %{conn: conn} do
|
||||
user = create_test_user(%{email: "editme@example.com"})
|
||||
{:ok, view, html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
assert html =~ "Edit User"
|
||||
assert html =~ "Change Password"
|
||||
assert has_element?(view, "input[name='user[email]'][value='editme@example.com']")
|
||||
assert html =~ "Check 'Change Password' above to set a new password for this user"
|
||||
end
|
||||
|
||||
test "shows admin password fields when enabled", %{conn: conn} do
|
||||
user = create_test_user(%{email: "editme@example.com"})
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
assert has_element?(view, "input[name='user[password]']")
|
||||
refute has_element?(view, "input[name='user[password_confirmation]']")
|
||||
assert render(view) =~ "Admin Note"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit user form - updates" do
|
||||
test "updates email without changing password", %{conn: conn} do
|
||||
user = create_test_user(%{email: "old@example.com"})
|
||||
original_password = user.hashed_password
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
view
|
||||
|> form("#user-form", user: %{email: "new@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts)
|
||||
assert to_string(updated_user.email) == "new@example.com"
|
||||
assert updated_user.hashed_password == original_password
|
||||
end
|
||||
|
||||
test "admin sets new password for user", %{conn: conn} do
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
original_password = user.hashed_password
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
view
|
||||
|> form("#user-form",
|
||||
user: %{
|
||||
email: "user@example.com",
|
||||
password: "newadminpassword123"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
assert_redirected(view, "/users")
|
||||
|
||||
updated_user = Ash.reload!(user, domain: Mv.Accounts)
|
||||
assert updated_user.hashed_password != original_password
|
||||
assert String.starts_with?(updated_user.hashed_password, "$2b$")
|
||||
end
|
||||
end
|
||||
|
||||
describe "edit user form - validation" do
|
||||
test "shows error for duplicate email", %{conn: conn} do
|
||||
_existing_user = create_test_user(%{email: "taken@example.com"})
|
||||
user_to_edit = create_test_user(%{email: "original@example.com"})
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user_to_edit.id}/edit")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("#user-form", user: %{email: "taken@example.com"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "has already been taken"
|
||||
end
|
||||
|
||||
test "shows error for invalid password", %{conn: conn} do
|
||||
user = create_test_user(%{email: "user@example.com"})
|
||||
{:ok, view, _html} = setup_live_view(conn, "/users/#{user.id}/edit")
|
||||
|
||||
view |> element("input[name='set_password']") |> render_click()
|
||||
|
||||
result =
|
||||
view
|
||||
|> form("#user-form",
|
||||
user: %{
|
||||
email: "user@example.com",
|
||||
password: "123"
|
||||
}
|
||||
)
|
||||
|> render_submit()
|
||||
|
||||
case result do
|
||||
{:error, {:live_redirect, %{to: "/users"}}} ->
|
||||
flunk("Expected validation error but form was submitted successfully")
|
||||
|
||||
html when is_binary(html) ->
|
||||
assert html =~ "must have length of at least 8"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "internationalization" do
|
||||
test "shows German labels", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn, %{email: "admin_de@example.com"})
|
||||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||||
{:ok, _view, html} = live(conn, "/users/new")
|
||||
|
||||
assert html =~ "Neuer Benutzer"
|
||||
assert html =~ "E-Mail"
|
||||
assert html =~ "Passwort setzen"
|
||||
end
|
||||
|
||||
test "shows English labels", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn, %{email: "admin_en@example.com"})
|
||||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||
{:ok, _view, html} = live(conn, "/users/new")
|
||||
|
||||
assert html =~ "New User"
|
||||
assert html =~ "Email"
|
||||
assert html =~ "Set Password"
|
||||
end
|
||||
|
||||
test "shows different labels for edit vs new", %{conn: conn} do
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
conn = conn_with_oidc_user(conn, %{email: "admin@example.com"})
|
||||
|
||||
{:ok, _view, new_html} = live(conn, "/users/new")
|
||||
{:ok, _view, edit_html} = live(conn, "/users/#{user.id}/edit")
|
||||
|
||||
assert new_html =~ "Set Password"
|
||||
assert edit_html =~ "Change Password"
|
||||
end
|
||||
end
|
||||
end
|
||||
412
test/mv_web/user_live/index_test.exs
Normal file
412
test/mv_web/user_live/index_test.exs
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
defmodule MvWeb.UserLive.IndexTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "basic functionality" do
|
||||
test "shows translated title in German", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
assert html =~ "Benutzer auflisten"
|
||||
end
|
||||
|
||||
test "shows translated title in English", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
assert html =~ "Listing Users"
|
||||
end
|
||||
|
||||
test "shows New User button", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
assert html =~ "New User"
|
||||
end
|
||||
|
||||
test "displays users in a table", %{conn: conn} do
|
||||
# Create test users
|
||||
_user1 = create_test_user(%{email: "alice@example.com", oidc_id: "alice123"})
|
||||
_user2 = create_test_user(%{email: "bob@example.com", oidc_id: "bob456"})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ "alice@example.com"
|
||||
assert html =~ "bob@example.com"
|
||||
assert html =~ "alice123"
|
||||
assert html =~ "bob456"
|
||||
end
|
||||
|
||||
test "shows correct action links", %{conn: conn} do
|
||||
user = create_test_user(%{email: "test@example.com"})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ "Edit"
|
||||
assert html =~ "Delete"
|
||||
assert html =~ ~r/href="[^"]*\/users\/#{user.id}\/edit"/
|
||||
end
|
||||
end
|
||||
|
||||
describe "sorting functionality" do
|
||||
setup do
|
||||
# Create users with different emails for sorting tests
|
||||
user_a = create_test_user(%{email: "alpha@example.com", oidc_id: "alpha"})
|
||||
user_z = create_test_user(%{email: "zulu@example.com", oidc_id: "zulu"})
|
||||
user_m = create_test_user(%{email: "mike@example.com", oidc_id: "mike"})
|
||||
|
||||
%{users: [user_a, user_z, user_m]}
|
||||
end
|
||||
|
||||
test "initially sorts by email ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
# Should show ascending indicator (up arrow)
|
||||
assert html =~ "hero-chevron-up"
|
||||
assert html =~ ~s(aria-sort="ascending")
|
||||
|
||||
# Test actual sort order: alpha should appear before mike, mike before zulu
|
||||
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
|
||||
mike_pos = html |> :binary.match("mike@example.com") |> elem(0)
|
||||
zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0)
|
||||
|
||||
assert alpha_pos < mike_pos, "alpha@example.com should appear before mike@example.com"
|
||||
assert mike_pos < zulu_pos, "mike@example.com should appear before zulu@example.com"
|
||||
end
|
||||
|
||||
test "can sort email descending by clicking sort button", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Click on email sort button and get rendered result
|
||||
html = view |> element("button[phx-value-field='email']") |> render_click()
|
||||
|
||||
# Should now show descending indicator (down arrow)
|
||||
assert html =~ "hero-chevron-down"
|
||||
assert html =~ ~s(aria-sort="descending")
|
||||
|
||||
# Test actual sort order reversed: zulu should now appear before mike, mike before alpha
|
||||
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
|
||||
mike_pos = html |> :binary.match("mike@example.com") |> elem(0)
|
||||
zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0)
|
||||
|
||||
assert zulu_pos < mike_pos,
|
||||
"zulu@example.com should appear before mike@example.com when sorted desc"
|
||||
|
||||
assert mike_pos < alpha_pos,
|
||||
"mike@example.com should appear before alpha@example.com when sorted desc"
|
||||
end
|
||||
|
||||
test "toggles back to ascending when clicking sort button twice", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Click twice to toggle: asc -> desc -> asc
|
||||
view |> element("button[phx-value-field='email']") |> render_click()
|
||||
html = view |> element("button[phx-value-field='email']") |> render_click()
|
||||
|
||||
# Should be back to ascending
|
||||
assert html =~ "hero-chevron-up"
|
||||
assert html =~ ~s(aria-sort="ascending")
|
||||
|
||||
# Should be back to original ascending order
|
||||
alpha_pos = html |> :binary.match("alpha@example.com") |> elem(0)
|
||||
mike_pos = html |> :binary.match("mike@example.com") |> elem(0)
|
||||
zulu_pos = html |> :binary.match("zulu@example.com") |> elem(0)
|
||||
|
||||
assert alpha_pos < mike_pos, "Should be back to ascending: alpha before mike"
|
||||
assert mike_pos < zulu_pos, "Should be back to ascending: mike before zulu"
|
||||
end
|
||||
|
||||
test "shows sort direction icons", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Initially ascending - should show up arrow
|
||||
html = render(view)
|
||||
assert html =~ "hero-chevron-up"
|
||||
|
||||
# After clicking, should show down arrow
|
||||
view |> element("button[phx-value-field='email']") |> render_click()
|
||||
html = render(view)
|
||||
assert html =~ "hero-chevron-down"
|
||||
end
|
||||
end
|
||||
|
||||
describe "checkbox selection functionality" do
|
||||
setup do
|
||||
user1 = create_test_user(%{email: "user1@example.com", oidc_id: "user1"})
|
||||
user2 = create_test_user(%{email: "user2@example.com", oidc_id: "user2"})
|
||||
%{users: [user1, user2]}
|
||||
end
|
||||
|
||||
test "shows select all checkbox", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ ~s(name="select_all")
|
||||
assert html =~ ~s(phx-click="select_all")
|
||||
end
|
||||
|
||||
test "shows individual user checkboxes", %{conn: conn, users: [user1, user2]} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ ~s(name="#{user1.id}")
|
||||
assert html =~ ~s(name="#{user2.id}")
|
||||
assert html =~ ~s(phx-click="select_user")
|
||||
end
|
||||
|
||||
test "can select individual users", %{conn: conn, users: [user1, user2]} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Initially, individual checkboxes should exist but not be checked
|
||||
assert view |> element("input[type='checkbox'][name='#{user1.id}']") |> has_element?()
|
||||
assert view |> element("input[type='checkbox'][name='#{user2.id}']") |> has_element?()
|
||||
|
||||
# Initially, select_all should not be checked (since no individual items are selected)
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Select first user checkbox
|
||||
html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click()
|
||||
|
||||
# The select_all checkbox should still not be checked (not all users selected)
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Page should still function normally
|
||||
assert html =~ "Email"
|
||||
assert html =~ to_string(user1.email)
|
||||
end
|
||||
|
||||
test "can deselect individual users", %{conn: conn, users: [user1, _user2]} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Select user first
|
||||
view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click()
|
||||
|
||||
# Then deselect user
|
||||
html = view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click()
|
||||
|
||||
# Select all should not be checked after deselecting individual user
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Page should still function normally
|
||||
assert html =~ "Email"
|
||||
assert html =~ to_string(user1.email)
|
||||
end
|
||||
|
||||
test "select all functionality selects all users", %{conn: conn, users: [user1, user2]} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Initially no checkboxes should be checked
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='#{user1.id}'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='#{user2.id}'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Click select all
|
||||
html = view |> element("input[type='checkbox'][name='select_all']") |> render_click()
|
||||
|
||||
# After selecting all, the select_all checkbox should be checked
|
||||
assert view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Page should still function normally and show all users
|
||||
assert html =~ "Email"
|
||||
assert html =~ to_string(user1.email)
|
||||
assert html =~ to_string(user2.email)
|
||||
end
|
||||
|
||||
test "deselect all functionality deselects all users", %{conn: conn, users: [user1, user2]} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Select all first
|
||||
view |> element("input[type='checkbox'][name='select_all']") |> render_click()
|
||||
|
||||
# Verify that select_all is checked
|
||||
assert view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Then deselect all
|
||||
html = view |> element("input[type='checkbox'][name='select_all']") |> render_click()
|
||||
|
||||
# After deselecting all, no checkboxes should be checked
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='#{user1.id}'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='#{user2.id}'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Page should still function normally
|
||||
assert html =~ "Email"
|
||||
assert html =~ to_string(user1.email)
|
||||
assert html =~ to_string(user2.email)
|
||||
end
|
||||
|
||||
test "select all automatically checks when all individual users are selected", %{
|
||||
conn: conn,
|
||||
users: [user1, user2]
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Initially nothing should be checked
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Select first user
|
||||
view |> element("input[type='checkbox'][name='#{user1.id}']") |> render_click()
|
||||
# Select all should still not be checked (only 1 of 2+ users selected)
|
||||
refute view
|
||||
|> element("input[type='checkbox'][name='select_all'][checked]")
|
||||
|> has_element?()
|
||||
|
||||
# Select second user
|
||||
html = view |> element("input[type='checkbox'][name='#{user2.id}']") |> render_click()
|
||||
|
||||
# Now select all should be automatically checked (all individual users are selected)
|
||||
# Note: This test might need adjustment based on actual implementation
|
||||
# The logic depends on whether authenticated user is included in the count
|
||||
assert html =~ "Email"
|
||||
assert html =~ to_string(user1.email)
|
||||
assert html =~ to_string(user2.email)
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete functionality" do
|
||||
test "can delete a user", %{conn: conn} do
|
||||
_user = create_test_user(%{email: "delete-me@example.com"})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# Confirm user is displayed
|
||||
assert render(view) =~ "delete-me@example.com"
|
||||
|
||||
# Click the first delete button to test the functionality
|
||||
view |> element("tbody tr:first-child a[data-confirm]") |> render_click()
|
||||
|
||||
# The page should still render (basic functionality test)
|
||||
html = render(view)
|
||||
# Table header should still be there
|
||||
assert html =~ "Email"
|
||||
end
|
||||
|
||||
test "shows delete confirmation", %{conn: conn} do
|
||||
_user = create_test_user(%{email: "confirm-delete@example.com"})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
# Check that delete link has confirmation attribute
|
||||
assert html =~ ~s(data-confirm="Are you sure?")
|
||||
end
|
||||
end
|
||||
|
||||
describe "navigation" do
|
||||
test "clicking on user row navigates to user show page", %{conn: conn} do
|
||||
user = create_test_user(%{email: "navigate@example.com"})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/users")
|
||||
|
||||
# This test would need to check row click behavior
|
||||
# The actual navigation would happen via JavaScript
|
||||
html = render(view)
|
||||
assert html =~ ~s(/users/#{user.id})
|
||||
end
|
||||
|
||||
test "edit link points to correct edit page", %{conn: conn} do
|
||||
user = create_test_user(%{email: "edit-me@example.com"})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ ~s(/users/#{user.id}/edit)
|
||||
end
|
||||
|
||||
test "new user button points to correct new page", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ ~s(/users/new)
|
||||
end
|
||||
end
|
||||
|
||||
describe "translations" do
|
||||
test "shows German translations for selection", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ "Alle Benutzer auswählen"
|
||||
assert html =~ "Benutzer auswählen"
|
||||
end
|
||||
|
||||
test "shows English translations for selection", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
# Note: English translations might be empty strings by default
|
||||
# This test would verify the structure is there
|
||||
# Checking that aria-label attributes exist
|
||||
assert html =~ ~s(aria-label=)
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "handles empty user list gracefully", %{conn: conn} do
|
||||
# Don't create any users besides the authenticated one
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
# Should still show the table structure
|
||||
assert html =~ "Email"
|
||||
assert html =~ "OIDC ID"
|
||||
# Should show the authenticated user at minimum
|
||||
assert html =~ "user@example.com"
|
||||
end
|
||||
|
||||
test "handles users with missing OIDC ID", %{conn: conn} do
|
||||
_user = create_test_user(%{email: "no-oidc@example.com", oidc_id: nil})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ "no-oidc@example.com"
|
||||
# Should handle nil OIDC ID gracefully
|
||||
end
|
||||
|
||||
test "handles very long email addresses", %{conn: conn} do
|
||||
long_email = "very.long.email.address.that.might.break.layouts@example.com"
|
||||
_user = create_test_user(%{email: long_email})
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/users")
|
||||
|
||||
assert html =~ long_email
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -33,16 +33,57 @@ defmodule MvWeb.ConnCase do
|
|||
|
||||
@doc """
|
||||
Creates a test user and returns the user struct.
|
||||
Accepts attrs to override default values.
|
||||
|
||||
Password handling:
|
||||
- If `hashed_password` is provided in attrs, it's used directly
|
||||
- If `password` is provided in attrs, it gets hashed automatically
|
||||
- If neither is provided, uses default password "password"
|
||||
|
||||
## Examples
|
||||
|
||||
create_test_user() # Default user with unique email
|
||||
create_test_user(%{email: "custom@example.com"}) # Custom email
|
||||
create_test_user(%{password: "secret123"}) # Custom password (gets hashed)
|
||||
create_test_user(%{hashed_password: "$2b$..."}) # Pre-hashed password
|
||||
"""
|
||||
def create_test_user(attrs \\ %{}) do
|
||||
email = "user@example.com"
|
||||
password = "password"
|
||||
{:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password)
|
||||
# Generate unique values to avoid conflicts
|
||||
unique_id = System.unique_integer([:positive])
|
||||
|
||||
Ash.Seed.seed!(Mv.Accounts.User, %{
|
||||
email: email,
|
||||
hashed_password: hashed_password
|
||||
})
|
||||
default_attrs = %{
|
||||
email: "user#{unique_id}@example.com",
|
||||
oidc_id: "oidc#{unique_id}"
|
||||
}
|
||||
|
||||
# Merge provided attrs with defaults
|
||||
user_attrs = Map.merge(default_attrs, attrs)
|
||||
|
||||
# Handle password/hashed_password
|
||||
final_attrs =
|
||||
cond do
|
||||
# If hashed_password is already provided, use it as-is
|
||||
Map.has_key?(user_attrs, :hashed_password) ->
|
||||
user_attrs
|
||||
|
||||
# If password is provided, hash it
|
||||
Map.has_key?(user_attrs, :password) ->
|
||||
password = Map.get(user_attrs, :password)
|
||||
{:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password)
|
||||
|
||||
user_attrs
|
||||
# Remove plain password
|
||||
|> Map.delete(:password)
|
||||
|> Map.put(:hashed_password, hashed_password)
|
||||
|
||||
# Neither provided, use default password
|
||||
true ->
|
||||
password = "password"
|
||||
{:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password)
|
||||
Map.put(user_attrs, :hashed_password, hashed_password)
|
||||
end
|
||||
|
||||
Ash.Seed.seed!(Mv.Accounts.User, final_attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -57,8 +98,9 @@ defmodule MvWeb.ConnCase do
|
|||
|
||||
@doc """
|
||||
Signs in a user via OIDC and returns a connection with the user authenticated.
|
||||
By default creates a user with "user@example.com" for consistency.
|
||||
"""
|
||||
def conn_with_oidc_user(conn, user_attrs \\ %{}) do
|
||||
def conn_with_oidc_user(conn, user_attrs \\ %{email: "user@example.com"}) do
|
||||
user = create_test_user(user_attrs)
|
||||
sign_in_user_via_oidc(conn, user)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue