Code documentation and refactoring #201
28 changed files with 689 additions and 55 deletions
|
|
@ -158,11 +158,11 @@
|
||||||
{Credo.Check.Warning.UnusedRegexOperation, []},
|
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||||
{Credo.Check.Warning.UnusedStringOperation, []},
|
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||||
{Credo.Check.Warning.UnusedTupleOperation, []},
|
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||||
{Credo.Check.Warning.WrongTestFileExtension, []}
|
{Credo.Check.Warning.WrongTestFileExtension, []},
|
||||||
|
# Module documentation check (enabled after adding @moduledoc to all modules)
|
||||||
|
{Credo.Check.Readability.ModuleDoc, []}
|
||||||
],
|
],
|
||||||
disabled: [
|
disabled: [
|
||||||
# Checks disabled by the Mitgliederverwaltung Team
|
|
||||||
{Credo.Check.Readability.ModuleDoc, []},
|
|
||||||
#
|
#
|
||||||
# Checks scheduled for next check update (opt-in for now)
|
# Checks scheduled for next check update (opt-in for now)
|
||||||
{Credo.Check.Refactor.UtcNowTruncate, []},
|
{Credo.Check.Refactor.UtcNowTruncate, []},
|
||||||
|
|
|
||||||
|
|
@ -917,14 +917,16 @@ mix credo --strict
|
||||||
|
|
||||||
- Consistency checks (spacing, line endings, parameter patterns)
|
- Consistency checks (spacing, line endings, parameter patterns)
|
||||||
- Design checks (FIXME/TODO tags, alias usage)
|
- Design checks (FIXME/TODO tags, alias usage)
|
||||||
- Readability checks (max line length: 120, module/function names)
|
- Readability checks (max line length: 120, module/function names, **module documentation**)
|
||||||
- Refactoring opportunities (cyclomatic complexity, nesting)
|
- Refactoring opportunities (cyclomatic complexity, nesting)
|
||||||
- Warnings (unused operations, unsafe operations)
|
- Warnings (unused operations, unsafe operations)
|
||||||
|
|
||||||
**Disabled Checks:**
|
**Documentation Enforcement:**
|
||||||
|
|
||||||
- `Credo.Check.Readability.ModuleDoc` - Disabled by team decision
|
- ✅ `Credo.Check.Readability.ModuleDoc` - **ENABLED** (as of November 2025)
|
||||||
(Still encouraged to add module docs for public modules)
|
- All modules require `@moduledoc` documentation
|
||||||
|
- Current coverage: 51 @moduledoc declarations across 47 modules (100% core modules)
|
||||||
|
- CI pipeline enforces documentation standards
|
||||||
|
|
||||||
**Address Credo Issues:**
|
**Address Credo Issues:**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,37 @@
|
||||||
defmodule Mv.Membership.Email do
|
defmodule Mv.Membership.Email do
|
||||||
|
@moduledoc """
|
||||||
|
Custom Ash type for validated email addresses.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This type extends `:string` with email-specific validation constraints.
|
||||||
|
It ensures that email values stored in Property resources are valid email
|
||||||
|
addresses according to a standard regex pattern.
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
- Minimum length: 5 characters
|
||||||
|
- Maximum length: 254 characters (RFC 5321 maximum)
|
||||||
|
- Pattern: Standard email format (username@domain.tld)
|
||||||
|
- Automatic trimming of leading/trailing whitespace
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
This type is used in the Property union type for properties with
|
||||||
|
`value_type: :email` in PropertyType definitions.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
# In a property type definition
|
||||||
|
PropertyType.create!(%{
|
||||||
|
name: "work_email",
|
||||||
|
value_type: :email
|
||||||
|
})
|
||||||
|
|
||||||
|
# Valid values
|
||||||
|
"user@example.com"
|
||||||
|
"first.last@company.co.uk"
|
||||||
|
|
||||||
|
# Invalid values
|
||||||
|
"not-an-email" # Missing @ and domain
|
||||||
|
"a@b" # Too short
|
||||||
|
"""
|
||||||
@match_pattern ~S/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
@match_pattern ~S/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
|
||||||
@match_regex Regex.compile!(@match_pattern)
|
@match_regex Regex.compile!(@match_pattern)
|
||||||
@min_length 5
|
@min_length 5
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
defmodule Mv.Membership.Member do
|
defmodule Mv.Membership.Member do
|
||||||
|
@moduledoc """
|
||||||
|
Ash resource representing a club member.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Members are the core entity in the membership management system. Each member
|
||||||
|
can have:
|
||||||
|
- Personal information (name, email, phone, address)
|
||||||
|
- Optional link to a User account (1:1 relationship)
|
||||||
|
- Dynamic custom properties via PropertyType system
|
||||||
|
- Full-text searchable profile
|
||||||
|
|
||||||
|
## Email Synchronization
|
||||||
|
When a member is linked to a user account, emails are automatically synchronized
|
||||||
|
bidirectionally. User.email is the source of truth on initial link.
|
||||||
|
See `Mv.EmailSync` for details.
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
- `has_many :properties` - Dynamic custom fields
|
||||||
|
- `has_one :user` - Optional authentication account link
|
||||||
|
|
||||||
|
## Validations
|
||||||
|
- Required: first_name, last_name, email
|
||||||
|
- Email format validation (using EctoCommons.EmailValidator)
|
||||||
|
- Phone number format: international format with 6-20 digits
|
||||||
|
- Postal code format: exactly 5 digits (German format)
|
||||||
|
- Date validations: birth_date and join_date not in future, exit_date after join_date
|
||||||
|
- Email uniqueness: prevents conflicts with unlinked users
|
||||||
|
|
||||||
|
## Full-Text Search
|
||||||
|
Members have a `search_vector` attribute (tsvector) that is automatically
|
||||||
|
updated via database trigger. Search includes name, email, notes, and contact fields.
|
||||||
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
defmodule Mv.Membership do
|
defmodule Mv.Membership do
|
||||||
|
@moduledoc """
|
||||||
|
Ash Domain for membership management.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- `Member` - Club members with personal information and custom properties
|
||||||
|
- `Property` - Dynamic custom field values attached to members
|
||||||
|
- `PropertyType` - Schema definitions for custom properties
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
The domain exposes these main actions:
|
||||||
|
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
|
||||||
|
- Property management: `create_property/1`, `list_property/0`, etc.
|
||||||
|
- PropertyType management: `create_property_type/1`, `list_property_types/0`, etc.
|
||||||
|
|
||||||
|
## Admin Interface
|
||||||
|
The domain is configured with AshAdmin for management UI.
|
||||||
|
"""
|
||||||
use Ash.Domain,
|
use Ash.Domain,
|
||||||
extensions: [AshAdmin.Domain, AshPhoenix]
|
extensions: [AshAdmin.Domain, AshPhoenix]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
defmodule Mv.Membership.Property do
|
defmodule Mv.Membership.Property do
|
||||||
|
@moduledoc """
|
||||||
|
Ash resource representing a custom property value for a member.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Properties implement the Entity-Attribute-Value (EAV) pattern, allowing
|
||||||
|
dynamic custom fields to be attached to members. Each property links a
|
||||||
|
member to a property type and stores the actual value.
|
||||||
|
|
||||||
|
## Value Storage
|
||||||
|
Values are stored using Ash's union type with JSONB storage format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"value": "example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Types
|
||||||
|
- `:string` - Text data
|
||||||
|
- `:integer` - Numeric data
|
||||||
|
- `:boolean` - True/false flags
|
||||||
|
- `:date` - Date values
|
||||||
|
- `:email` - Validated email addresses (custom type)
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
- `belongs_to :member` - The member this property belongs to (CASCADE delete)
|
||||||
|
- `belongs_to :property_type` - The property type definition
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- Each member can have only one property per property type (unique composite index)
|
||||||
|
- Properties are deleted when the associated member is deleted (CASCADE)
|
||||||
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,48 @@
|
||||||
defmodule Mv.Membership.PropertyType do
|
defmodule Mv.Membership.PropertyType do
|
||||||
|
@moduledoc """
|
||||||
|
Ash resource defining the schema for custom member properties.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
PropertyTypes define the "schema" for custom fields in the membership system.
|
||||||
|
Each PropertyType specifies the name, data type, and behavior of a custom field
|
||||||
|
that can be attached to members via Property resources.
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
- `name` - Unique identifier for the property (e.g., "phone_mobile", "birthday")
|
||||||
|
- `value_type` - Data type constraint (`:string`, `:integer`, `:boolean`, `:date`, `:email`)
|
||||||
|
- `description` - Optional human-readable description
|
||||||
|
- `immutable` - If true, property values cannot be changed after creation
|
||||||
|
- `required` - If true, all members must have this property (future feature)
|
||||||
|
|
||||||
|
## Supported Value Types
|
||||||
|
- `:string` - Text data (unlimited length)
|
||||||
|
- `:integer` - Numeric data (64-bit integers)
|
||||||
|
- `:boolean` - True/false flags
|
||||||
|
- `:date` - Date values (no time component)
|
||||||
|
- `:email` - Validated email addresses
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
- `has_many :properties` - All property values of this type
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
- Name must be unique across all property types
|
||||||
|
- Cannot delete a property type that has existing property values (RESTRICT)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
# Create a new property type
|
||||||
|
PropertyType.create!(%{
|
||||||
|
name: "phone_mobile",
|
||||||
|
value_type: :string,
|
||||||
|
description: "Mobile phone number"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a required property type
|
||||||
|
PropertyType.create!(%{
|
||||||
|
name: "emergency_contact",
|
||||||
|
value_type: :string,
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,20 @@ defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do
|
||||||
|
|
||||||
alias Mv.Mailer
|
alias Mv.Mailer
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends a confirmation email to a new user.
|
||||||
|
|
||||||
|
This function is called automatically by AshAuthentication when a new
|
||||||
|
user registers and needs to confirm their email address.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `user` - The user record who needs to confirm their email
|
||||||
|
- `token` - The confirmation token to include in the email link
|
||||||
|
- `_opts` - Additional options (unused)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
The Swoosh.Email delivery result from `Mailer.deliver!/1`.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def send(user, token, _) do
|
def send(user, token, _) do
|
||||||
new()
|
new()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,20 @@ defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do
|
||||||
|
|
||||||
alias Mv.Mailer
|
alias Mv.Mailer
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends a password reset email to a user.
|
||||||
|
|
||||||
|
This function is called automatically by AshAuthentication when a user
|
||||||
|
requests a password reset.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `user` - The user record requesting the password reset
|
||||||
|
- `token` - The password reset token to include in the email link
|
||||||
|
- `_opts` - Additional options (unused)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
The Swoosh.Email delivery result from `Mailer.deliver!/1`.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def send(user, token, _) do
|
def send(user, token, _) do
|
||||||
new()
|
new()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@ defmodule Mv.Accounts.User.Validations.EmailNotUsedByOtherMember do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validates email uniqueness across linked User-Member pairs.
|
||||||
|
|
||||||
|
This validation ensures that when a user is linked to a member, their email
|
||||||
|
does not conflict with another member's email. It only runs when necessary
|
||||||
|
to avoid blocking valid operations (see `@moduledoc` for trigger conditions).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `changeset` - The Ash changeset being validated
|
||||||
|
- `_opts` - Options passed to the validation (unused)
|
||||||
|
- `_context` - Ash context map (unused)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
- `:ok` if validation passes or should be skipped
|
||||||
|
- `{:error, field: :email, message: ..., value: ...}` if validation fails
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def validate(changeset, _opts, _context) do
|
def validate(changeset, _opts, _context) do
|
||||||
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,21 @@ defmodule Mv.EmailSync.Changes.SyncMemberEmailToUser do
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
alias Mv.EmailSync.{Helpers, Loader}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Implements the email synchronization from Member to User.
|
||||||
|
|
||||||
|
This function is called automatically by Ash when the configured trigger
|
||||||
|
conditions are met (see `@moduledoc` for trigger details).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `changeset` - The Ash changeset being processed
|
||||||
|
- `_opts` - Options passed to the change (unused)
|
||||||
|
- `context` - Ash context map containing metadata (e.g., `:syncing_email` flag)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
Modified changeset with email synchronization applied, or original changeset
|
||||||
|
if recursion detected.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def change(changeset, _opts, context) do
|
def change(changeset, _opts, context) do
|
||||||
# Only recursion protection needed - trigger logic is in `where` clauses
|
# Only recursion protection needed - trigger logic is in `where` clauses
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,21 @@ defmodule Mv.EmailSync.Changes.SyncUserEmailToMember do
|
||||||
use Ash.Resource.Change
|
use Ash.Resource.Change
|
||||||
alias Mv.EmailSync.{Helpers, Loader}
|
alias Mv.EmailSync.{Helpers, Loader}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Implements the email synchronization from User to Member.
|
||||||
|
|
||||||
|
This function is called automatically by Ash when the configured trigger
|
||||||
|
conditions are met (see `@moduledoc` for trigger details).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `changeset` - The Ash changeset being processed
|
||||||
|
- `_opts` - Options passed to the change (unused)
|
||||||
|
- `context` - Ash context map containing metadata (e.g., `:syncing_email` flag)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
Modified changeset with email synchronization applied, or original changeset
|
||||||
|
if recursion detected.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def change(changeset, _opts, context) do
|
def change(changeset, _opts, context) do
|
||||||
# Only recursion protection needed - trigger logic is in `where` clauses
|
# Only recursion protection needed - trigger logic is in `where` clauses
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@ defmodule Mv.Membership.Member.Validations.EmailNotUsedByOtherUser do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validates email uniqueness across linked Member-User pairs.
|
||||||
|
|
||||||
|
This validation ensures that when a member is linked to a user, their email
|
||||||
|
does not conflict with another user's email. It only runs when necessary
|
||||||
|
to avoid blocking valid operations (see `@moduledoc` for trigger conditions).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
- `changeset` - The Ash changeset being validated
|
||||||
|
- `_opts` - Options passed to the validation (unused)
|
||||||
|
- `_context` - Ash context map (unused)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
- `:ok` if validation passes or should be skipped
|
||||||
|
- `{:error, field: :email, message: ..., value: ...}` if validation fails
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def validate(changeset, _opts, _context) do
|
def validate(changeset, _opts, _context) do
|
||||||
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
email_changing? = Ash.Changeset.changing_attribute?(changeset, :email)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
defmodule Mv.Secrets do
|
defmodule Mv.Secrets do
|
||||||
|
@moduledoc """
|
||||||
|
Secret provider for AshAuthentication.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Provides runtime configuration secrets for Ash Authentication strategies,
|
||||||
|
particularly for OIDC (Rauthy) authentication.
|
||||||
|
|
||||||
|
## Configuration Source
|
||||||
|
Secrets are read from the `:rauthy` key in the application configuration,
|
||||||
|
which is typically set in `config/runtime.exs` from environment variables:
|
||||||
|
- `OIDC_CLIENT_ID`
|
||||||
|
- `OIDC_CLIENT_SECRET`
|
||||||
|
- `OIDC_BASE_URL`
|
||||||
|
- `OIDC_REDIRECT_URI`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
This module is automatically called by AshAuthentication when resolving
|
||||||
|
secrets for the User resource's OIDC strategy.
|
||||||
|
"""
|
||||||
use AshAuthentication.Secret
|
use AshAuthentication.Secret
|
||||||
|
|
||||||
def secret_for(
|
def secret_for(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
defmodule MvWeb.AuthOverrides do
|
defmodule MvWeb.AuthOverrides do
|
||||||
|
@moduledoc """
|
||||||
|
UI customizations for AshAuthentication Phoenix components.
|
||||||
|
|
||||||
|
## Overrides
|
||||||
|
- `SignIn` - Restricts form width to prevent full-width display
|
||||||
|
- `Banner` - Replaces default logo with "Mitgliederverwaltung" text
|
||||||
|
- `HorizontalRule` - Translates "or" text to German
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
For complete reference on available overrides, see:
|
||||||
|
https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html
|
||||||
|
"""
|
||||||
use AshAuthentication.Phoenix.Overrides
|
use AshAuthentication.Phoenix.Overrides
|
||||||
use Gettext, backend: MvWeb.Gettext
|
use Gettext, backend: MvWeb.Gettext
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,33 @@
|
||||||
defmodule MvWeb.MemberLive.Form do
|
defmodule MvWeb.MemberLive.Form do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView form for creating and editing members.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Create new members with personal information
|
||||||
|
- Edit existing member details
|
||||||
|
- Manage custom properties (dynamic fields)
|
||||||
|
- Real-time validation with visual feedback
|
||||||
|
- Link/unlink user accounts
|
||||||
|
|
||||||
|
## Form Fields
|
||||||
|
**Required:**
|
||||||
|
- first_name, last_name, email
|
||||||
|
|
||||||
|
**Optional:**
|
||||||
|
- birth_date, phone_number, address fields (city, street, house_number, postal_code)
|
||||||
|
- join_date, exit_date
|
||||||
|
- paid status
|
||||||
|
- notes
|
||||||
|
|
||||||
|
## Custom Properties
|
||||||
|
Members can have dynamic custom properties defined by PropertyTypes.
|
||||||
|
The form dynamically renders inputs based on available PropertyTypes.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `validate` - Real-time form validation
|
||||||
|
- `save` - Submit form (create or update member)
|
||||||
|
- Property management events for adding/removing custom fields
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,37 @@
|
||||||
defmodule MvWeb.MemberLive.Index do
|
defmodule MvWeb.MemberLive.Index do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying and managing the member list.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Full-text search across member profiles using PostgreSQL tsvector
|
||||||
|
- Sortable columns (name, email, address fields)
|
||||||
|
- Bulk selection for future batch operations
|
||||||
|
- Real-time updates via LiveView
|
||||||
|
- Bookmarkable URLs with query parameters
|
||||||
|
|
||||||
|
## URL Parameters
|
||||||
|
- `query` - Search query string for full-text search
|
||||||
|
- `sort_field` - Field to sort by (e.g., :first_name, :email, :join_date)
|
||||||
|
- `sort_order` - Sort direction (:asc or :desc)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `delete` - Remove a member from the database
|
||||||
|
- `select_member` - Toggle individual member selection
|
||||||
|
- `select_all` - Toggle selection of all visible members
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
- Search uses PostgreSQL full-text search (plainto_tsquery)
|
||||||
|
- Sort state is synced with URL for bookmarkability
|
||||||
|
- Components communicate via `handle_info` for decoupling
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initializes the LiveView state.
|
||||||
|
|
||||||
|
Sets up initial assigns for page title, search query, sort configuration,
|
||||||
|
and member selection. Actual data loading happens in `handle_params/3`.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
socket =
|
socket =
|
||||||
|
|
@ -19,7 +50,14 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Handle Events
|
# Handle Events
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
# Delete a member
|
@doc """
|
||||||
|
Handles member-related UI events.
|
||||||
|
|
||||||
|
## Supported events:
|
||||||
|
- `"delete"` - Removes a member from the database
|
||||||
|
- `"select_member"` - Toggles individual member selection
|
||||||
|
- `"select_all"` - Toggles selection of all visible members
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
member = Ash.get!(Mv.Membership.Member, id)
|
member = Ash.get!(Mv.Membership.Member, id)
|
||||||
|
|
@ -29,7 +67,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:noreply, assign(socket, :members, updated_members)}
|
{:noreply, assign(socket, :members, updated_members)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Selects one member in the list of members
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("select_member", %{"id" => id}, socket) do
|
def handle_event("select_member", %{"id" => id}, socket) do
|
||||||
selected =
|
selected =
|
||||||
|
|
@ -42,7 +79,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
{:noreply, assign(socket, :selected_members, selected)}
|
{:noreply, assign(socket, :selected_members, selected)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Selects all members in the list of members
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("select_all", _params, socket) do
|
def handle_event("select_all", _params, socket) do
|
||||||
members = socket.assigns.members
|
members = socket.assigns.members
|
||||||
|
|
@ -63,57 +99,23 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# Handle Infos from Child Components
|
# Handle Infos from Child Components
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
# Sorts the list of members according to a field, when you click on the column header
|
@doc """
|
||||||
|
Handles messages from child components.
|
||||||
|
|
||||||
|
## Supported messages:
|
||||||
|
- `{:sort, field}` - Sort event from SortHeaderComponent. Updates sort field/order and syncs URL
|
||||||
|
- `{:search_changed, query}` - Search event from SearchBarComponent. Filters members and syncs URL
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:sort, field_str}, socket) do
|
def handle_info({:sort, field_str}, socket) do
|
||||||
field = String.to_existing_atom(field_str)
|
field = String.to_existing_atom(field_str)
|
||||||
old_field = socket.assigns.sort_field
|
{new_field, new_order} = determine_new_sort(field, socket)
|
||||||
|
|
||||||
{new_order, new_field} =
|
socket
|
||||||
if socket.assigns.sort_field == field do
|
|> update_sort_components(socket.assigns.sort_field, new_field, new_order)
|
||||||
{toggle_order(socket.assigns.sort_order), field}
|
|> push_sort_url(new_field, new_order)
|
||||||
else
|
|
||||||
{:asc, field}
|
|
||||||
end
|
|
||||||
|
|
||||||
active_id = :"sort_#{new_field}"
|
|
||||||
old_id = :"sort_#{old_field}"
|
|
||||||
|
|
||||||
# Update the new SortHeader
|
|
||||||
send_update(MvWeb.Components.SortHeaderComponent,
|
|
||||||
id: active_id,
|
|
||||||
sort_field: new_field,
|
|
||||||
sort_order: new_order
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reset the current SortHeader
|
|
||||||
send_update(MvWeb.Components.SortHeaderComponent,
|
|
||||||
id: old_id,
|
|
||||||
sort_field: new_field,
|
|
||||||
sort_order: new_order
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_search_query = socket.assigns.query
|
|
||||||
|
|
||||||
# Build the URL with queries
|
|
||||||
query_params = %{
|
|
||||||
"query" => existing_search_query,
|
|
||||||
"sort_field" => Atom.to_string(new_field),
|
|
||||||
"sort_order" => Atom.to_string(new_order)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set the new path with params
|
|
||||||
new_path = ~p"/members?#{query_params}"
|
|
||||||
|
|
||||||
# Push the new URL
|
|
||||||
{:noreply,
|
|
||||||
push_patch(socket,
|
|
||||||
to: new_path,
|
|
||||||
replace: true
|
|
||||||
)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Function to handle search
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:search_changed, q}, socket) do
|
def handle_info({:search_changed, q}, socket) do
|
||||||
socket = load_members(socket, q)
|
socket = load_members(socket, q)
|
||||||
|
|
@ -142,6 +144,13 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# Handle Params from the URL
|
# Handle Params from the URL
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
@doc """
|
||||||
|
Handles URL parameter changes.
|
||||||
|
|
||||||
|
Parses query parameters for search query, sort field, and sort order,
|
||||||
|
then loads members accordingly. This enables bookmarkable URLs and
|
||||||
|
browser back/forward navigation.
|
||||||
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(params, _url, socket) do
|
def handle_params(params, _url, socket) do
|
||||||
socket =
|
socket =
|
||||||
|
|
@ -156,6 +165,55 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# FUNCTIONS
|
# FUNCTIONS
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
# Determines new sort field and order based on current state
|
||||||
|
defp determine_new_sort(field, socket) do
|
||||||
|
if socket.assigns.sort_field == field do
|
||||||
|
{field, toggle_order(socket.assigns.sort_order)}
|
||||||
|
else
|
||||||
|
{field, :asc}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Updates both the active and old SortHeader components
|
||||||
|
defp update_sort_components(socket, old_field, new_field, new_order) do
|
||||||
|
active_id = :"sort_#{new_field}"
|
||||||
|
old_id = :"sort_#{old_field}"
|
||||||
|
|
||||||
|
# Update the new SortHeader
|
||||||
|
send_update(MvWeb.Components.SortHeaderComponent,
|
||||||
|
id: active_id,
|
||||||
|
sort_field: new_field,
|
||||||
|
sort_order: new_order
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the current SortHeader
|
||||||
|
send_update(MvWeb.Components.SortHeaderComponent,
|
||||||
|
id: old_id,
|
||||||
|
sort_field: new_field,
|
||||||
|
sort_order: new_order
|
||||||
|
)
|
||||||
|
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
# Builds sort URL and pushes navigation patch
|
||||||
|
defp push_sort_url(socket, field, order) do
|
||||||
|
query_params = %{
|
||||||
|
"query" => socket.assigns.query,
|
||||||
|
"sort_field" => Atom.to_string(field),
|
||||||
|
"sort_order" => Atom.to_string(order)
|
||||||
|
}
|
||||||
|
|
||||||
|
new_path = ~p"/members?#{query_params}"
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
push_patch(socket,
|
||||||
|
to: new_path,
|
||||||
|
replace: true
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
# Load members eg based on a query for sorting
|
# Load members eg based on a query for sorting
|
||||||
defp load_members(socket, search_query) do
|
defp load_members(socket, search_query) do
|
||||||
query =
|
query =
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
defmodule MvWeb.MemberLive.Show do
|
defmodule MvWeb.MemberLive.Show do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying a single member's details.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Display all member information (personal, contact, address)
|
||||||
|
- Show linked user account (if exists)
|
||||||
|
- Display custom properties
|
||||||
|
- Navigate to edit form
|
||||||
|
- Return to member list
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
- Basic: name, email, dates (birth, join, exit)
|
||||||
|
- Contact: phone number
|
||||||
|
- Address: street, house number, postal code, city
|
||||||
|
- Status: paid flag
|
||||||
|
- Relationships: linked user account
|
||||||
|
- Custom: dynamic properties from PropertyTypes
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
- Back to member list
|
||||||
|
- Edit member (with return_to parameter for back navigation)
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
import Ash.Query
|
import Ash.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,35 @@
|
||||||
defmodule MvWeb.PropertyLive.Form do
|
defmodule MvWeb.PropertyLive.Form do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView form for creating and editing properties.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Create new properties with member and type selection
|
||||||
|
- Edit existing property values
|
||||||
|
- Value input adapts to property type (string, integer, boolean, date, email)
|
||||||
|
- Real-time validation
|
||||||
|
|
||||||
|
## Form Fields
|
||||||
|
**Required:**
|
||||||
|
- member - Select which member owns this property
|
||||||
|
- property_type - Select the type (defines value type)
|
||||||
|
- value - The actual value (input type depends on property type)
|
||||||
|
|
||||||
|
## Value Types
|
||||||
|
The form dynamically renders appropriate inputs based on property type:
|
||||||
|
- String: text input
|
||||||
|
- Integer: number input
|
||||||
|
- Boolean: checkbox
|
||||||
|
- Date: date picker
|
||||||
|
- Email: email input with validation
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `validate` - Real-time form validation
|
||||||
|
- `save` - Submit form (create or update property)
|
||||||
|
|
||||||
|
## Note
|
||||||
|
Properties are typically managed through the member edit form,
|
||||||
|
not through this standalone form.
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
defmodule MvWeb.PropertyLive.Index do
|
defmodule MvWeb.PropertyLive.Index do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying and managing properties.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- List all properties with their values and types
|
||||||
|
- Show which member each property belongs to
|
||||||
|
- Display property type information
|
||||||
|
- Navigate to property details and edit forms
|
||||||
|
- Delete properties
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
Each property is linked to:
|
||||||
|
- A member (the property owner)
|
||||||
|
- A property type (defining value type and behavior)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `delete` - Remove a property from the database
|
||||||
|
|
||||||
|
## Note
|
||||||
|
Properties are typically managed through the member edit form.
|
||||||
|
This view provides a global overview of all properties.
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
defmodule MvWeb.PropertyLive.Show do
|
defmodule MvWeb.PropertyLive.Show do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying a single property's details.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Display property value and type
|
||||||
|
- Show linked member
|
||||||
|
- Show property type definition
|
||||||
|
- Navigate to edit form
|
||||||
|
- Return to property list
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
- Property value (formatted based on type)
|
||||||
|
- Property type name and description
|
||||||
|
- Member information (who owns this property)
|
||||||
|
- Property metadata (ID, timestamps if added)
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
- Back to property list
|
||||||
|
- Edit property
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,38 @@
|
||||||
defmodule MvWeb.PropertyTypeLive.Form do
|
defmodule MvWeb.PropertyTypeLive.Form do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView form for creating and editing property types (admin).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Create new property type definitions
|
||||||
|
- Edit existing property types
|
||||||
|
- Select value type from supported types
|
||||||
|
- Set immutable and required flags
|
||||||
|
- Real-time validation
|
||||||
|
|
||||||
|
## Form Fields
|
||||||
|
**Required:**
|
||||||
|
- name - Unique identifier (e.g., "phone_mobile", "emergency_contact")
|
||||||
|
- value_type - Data type (:string, :integer, :boolean, :date, :email)
|
||||||
|
|
||||||
|
**Optional:**
|
||||||
|
- description - Human-readable explanation
|
||||||
|
- immutable - If true, values cannot be changed after creation (default: false)
|
||||||
|
- required - If true, all members must have this property (default: false)
|
||||||
|
|
||||||
|
## Value Type Selection
|
||||||
|
- `:string` - Text data (unlimited length)
|
||||||
|
- `:integer` - Numeric data
|
||||||
|
- `:boolean` - True/false flags
|
||||||
|
- `:date` - Date values
|
||||||
|
- `:email` - Validated email addresses
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `validate` - Real-time form validation
|
||||||
|
- `save` - Submit form (create or update property type)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
Property type management is restricted to admin users.
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,28 @@
|
||||||
defmodule MvWeb.PropertyTypeLive.Index do
|
defmodule MvWeb.PropertyTypeLive.Index do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for managing property type definitions (admin).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- List all property types
|
||||||
|
- Display type information (name, value type, description)
|
||||||
|
- Show immutable and required flags
|
||||||
|
- Create new property types
|
||||||
|
- Edit existing property types
|
||||||
|
- Delete property types (if no properties use them)
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
- Name: Unique identifier for the property type
|
||||||
|
- Value type: Data type constraint (string, integer, boolean, date, email)
|
||||||
|
- Description: Human-readable explanation
|
||||||
|
- Immutable: Whether property values can be changed after creation
|
||||||
|
- Required: Whether all members must have this property (future feature)
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `delete` - Remove a property type (only if no properties exist)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
Property type management is restricted to admin users.
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,27 @@
|
||||||
defmodule MvWeb.PropertyTypeLive.Show do
|
defmodule MvWeb.PropertyTypeLive.Show do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying a single property type's details (admin).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Display property type definition
|
||||||
|
- Show all attributes (name, value type, description, flags)
|
||||||
|
- Navigate to edit form
|
||||||
|
- Return to property type list
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
- Name: Unique identifier
|
||||||
|
- Value type: Data type constraint
|
||||||
|
- Description: Optional explanation
|
||||||
|
- Immutable flag: Whether values can be changed
|
||||||
|
- Required flag: Whether all members need this property
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
- Back to property type list
|
||||||
|
- Edit property type
|
||||||
|
|
||||||
|
## Security
|
||||||
|
Property type details are restricted to admin users.
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,36 @@
|
||||||
defmodule MvWeb.UserLive.Form do
|
defmodule MvWeb.UserLive.Form do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView form for creating and editing users.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Create new users with email
|
||||||
|
- Edit existing user details
|
||||||
|
- Optional password setting (checkbox to toggle)
|
||||||
|
- Link/unlink member accounts
|
||||||
|
- Email synchronization with linked members
|
||||||
|
|
||||||
|
## Form Fields
|
||||||
|
**Required:**
|
||||||
|
- email
|
||||||
|
|
||||||
|
**Optional:**
|
||||||
|
- password (for password authentication strategy)
|
||||||
|
- linked member (select from existing members)
|
||||||
|
|
||||||
|
## Password Management
|
||||||
|
- New users: Can optionally set password with confirmation
|
||||||
|
- Existing users: Can change password (no confirmation required, admin action)
|
||||||
|
- Checkbox toggles password section visibility
|
||||||
|
|
||||||
|
## Member Linking
|
||||||
|
Users can be linked to existing member accounts. When linked, emails are
|
||||||
|
synchronized bidirectionally with User.email as the source of truth.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `validate` - Real-time form validation
|
||||||
|
- `save` - Submit form (create or update user)
|
||||||
|
- `toggle_password_section` - Show/hide password fields
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,25 @@
|
||||||
defmodule MvWeb.UserLive.Index do
|
defmodule MvWeb.UserLive.Index do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying and managing the user list.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- List all users with email and linked member
|
||||||
|
- Sort users by email (default)
|
||||||
|
- Delete users
|
||||||
|
- Navigate to user details and edit forms
|
||||||
|
- Bulk selection for future batch operations
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
Displays linked member information when a user is connected to a member account.
|
||||||
|
|
||||||
|
## Events
|
||||||
|
- `delete` - Remove a user from the database
|
||||||
|
- `select_user` - Toggle individual user selection
|
||||||
|
- `select_all` - Toggle selection of all visible users
|
||||||
|
|
||||||
|
## Security
|
||||||
|
User deletion requires admin permissions (enforced by Ash policies).
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
import MvWeb.TableComponents
|
import MvWeb.TableComponents
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,29 @@
|
||||||
defmodule MvWeb.UserLive.Show do
|
defmodule MvWeb.UserLive.Show do
|
||||||
|
@moduledoc """
|
||||||
|
LiveView for displaying a single user's details.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Display user information (email, OIDC ID)
|
||||||
|
- Show authentication methods (password, OIDC)
|
||||||
|
- Display linked member account (if exists)
|
||||||
|
- Navigate to edit form
|
||||||
|
- Return to user list
|
||||||
|
|
||||||
|
## Displayed Information
|
||||||
|
- Email address
|
||||||
|
- OIDC ID (if authenticated via OIDC)
|
||||||
|
- Password authentication status
|
||||||
|
- Linked member (name and email)
|
||||||
|
|
||||||
|
## Authentication Status
|
||||||
|
Shows which authentication methods are enabled for the user:
|
||||||
|
- Password authentication (has hashed_password)
|
||||||
|
- OIDC authentication (has oidc_id)
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
- Back to user list
|
||||||
|
- Edit user (with return_to parameter for back navigation)
|
||||||
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,16 @@
|
||||||
defmodule MvWeb.LiveHelpers do
|
defmodule MvWeb.LiveHelpers do
|
||||||
|
@moduledoc """
|
||||||
|
Shared LiveView lifecycle hooks and helper functions.
|
||||||
|
|
||||||
|
## on_mount Hooks
|
||||||
|
- `:default` - Sets the user's locale from session (defaults to "de")
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Add to LiveView modules via:
|
||||||
|
```elixir
|
||||||
|
on_mount {MvWeb.LiveHelpers, :default}
|
||||||
|
```
|
||||||
|
"""
|
||||||
def on_mount(:default, _params, session, socket) do
|
def on_mount(:default, _params, session, socket) do
|
||||||
locale = session["locale"] || "de"
|
locale = session["locale"] || "de"
|
||||||
Gettext.put_locale(locale)
|
Gettext.put_locale(locale)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue