README.md

# SelectoComponents

> ⚠️ **Alpha Quality Software**
>
> `selecto_components` is under active development. Expect breaking changes,
> behavior changes, incomplete features, and potentially major bugs.

Phoenix LiveView components for building interactive data query interfaces with [Selecto](https://github.com/selecto-elixir/selecto).

## Overview

SelectoComponents provides a suite of Phoenix LiveView components that enable users to build complex queries, visualize data, and interact with Ecto-based schemas through a visual interface. The library includes:

- **Query Builder**: Drag-and-drop interface for building complex filter queries
- **Data Views**: Multiple visualization options (Detail, Aggregate, Graph)
- **Colocated JavaScript**: Phoenix LiveView 1.1+ colocated hooks for drag-and-drop and charts
- **Tailwind CSS**: Pre-styled components using Tailwind CSS

## Release Status (0.3.x)

- **Alpha**: Core query UI flows (`SelectoComponents.Form`, result rendering,
  built-in views) are usable but not yet stable.
- **High Risk / Experimental**: Graph/dashboard and advanced integration paths
  may change significantly and can require project-specific hardening.
- **Maintenance Note**: Unwired experimental modules were pruned in 0.3.x to
  reduce surface area; only documented core flows are kept.
- **Not Included**: Turnkey production analytics/data-backend integration is
  outside current package scope.

## Requirements

- Phoenix 1.7+ (includes Phoenix LiveView compiler and esbuild with NODE_PATH)
- Elixir ~> 1.14
- Selecto ~> 0.3.1 (core library)
- selecto_mix ~> 0.3.0 (for code generation and integration tasks)

## Installation

### 1. Add Dependencies

In your `mix.exs`:

```elixir
def deps do
  [
    {:selecto_components, "~> 0.3.1"},
    {:selecto, "~> 0.3.1"},
    {:selecto_mix, "~> 0.3.0"}  # For generators and integration
  ]
end
```

Then install:
```bash
mix deps.get
```

### 2. Quick Setup (Recommended)

```bash
# Automatically integrate hooks and styles
mix selecto.components.integrate

# Build assets
mix assets.build
```

That's it! The integration task automatically:
- Adds SelectoComponents hooks to your app.js
- Adds Tailwind @source directive to your app.css

### 3. Manual Setup (Alternative)

If you prefer to configure manually or the integration task doesn't work:

#### In `assets/css/app.css`:
```css
/* Add this @source directive for SelectoComponents styles */
@source "../../deps/selecto_components/lib/**/*.{ex,heex}";
```

#### In `assets/js/app.js`:
```javascript
// Add this import at the top
import {hooks as selectoHooks} from "phoenix-colocated/selecto_components"

// In your LiveSocket configuration, spread the hooks:
const liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {
    ...selectoHooks,  // Add this line
    // your other hooks...
  }
})
```

Then build assets:
```bash
mix assets.build
```

## Usage

### Step 1: Generate a Domain

Use `selecto_mix` to generate a domain from your Ecto schema:

```bash
# Generate domain configuration only
mix selecto.gen.domain MyApp.Catalog.Product

# Generate with LiveView (recommended - includes integration)
mix selecto.gen.domain MyApp.Catalog.Product --live

# Generate with saved views support
mix selecto.gen.domain MyApp.Catalog.Product --live --saved-views
```

This creates:
- `lib/my_app/selecto_domains/product_domain.ex` - Domain configuration
- `lib/my_app_web/live/product_live.ex` - LiveView with SelectoComponents (if --live)
- Automatically runs `mix selecto.components.integrate` (if --live)

### Step 2: Use in LiveView

If you generated with `--live`, a LiveView is created for you. Otherwise, create one:

```elixir
defmodule MyAppWeb.ProductLive do
  use MyAppWeb, :live_view
  use SelectoComponents.Form  # Adds form handling utilities
  
  alias MyApp.SelectoDomains.ProductDomain
  alias MyApp.Repo
  
  @impl true
  def mount(_params, _session, socket) do
    # Initialize domain and selecto
    selecto = ProductDomain.new(Repo)
    domain = ProductDomain.domain()
    
    # Configure available views
    views = [
      {:detail, SelectoComponents.Views.Detail, "Table View", %{}},
      {:aggregate, SelectoComponents.Views.Aggregate, "Summary", %{}},
      {:graph, SelectoComponents.Views.Graph, "Charts", %{}}
    ]
    
    # Initialize state (from SelectoComponents.Form)
    state = get_initial_state(views, selecto)
    
    {:ok, assign(socket, state)}
  end
  
  @impl true
  def render(assigns) do
    ~H"""
    <div class="p-4">
      <h1 class="text-2xl mb-4">Product Explorer</h1>
      
      <.live_component
        module={SelectoComponents.Form}
        id="product-form"
        {assigns}
      />
      
      <.live_component
        module={SelectoComponents.Results}
        id="product-results"
        {assigns}
      />
    </div>
    """
  end
end
```

## Available Components

### Views Module
- `SelectoComponents.Views` - Main component that provides tabbed interface for different view types

### View Types
- `SelectoComponents.Views.Detail` - Table view with sortable columns and pagination
- `SelectoComponents.Views.Aggregate` - Aggregated data view with grouping capabilities
- `SelectoComponents.Views.Graph` - Chart visualization using Chart.js

### Custom View Systems

SelectoComponents now supports a formal view-system contract via
`SelectoComponents.Views.System`.

You can publish external view packages (for example
`selecto_components_workflow` or `selecto_components_faceted_product`) by
exposing a top-level view module that implements the behavior.

```elixir
defmodule SelectoComponentsWorkflow.Views.Workflow do
  use SelectoComponents.Views.System,
    process: SelectoComponentsWorkflow.Views.Workflow.Process,
    form: SelectoComponentsWorkflow.Views.Workflow.Form,
    component: SelectoComponentsWorkflow.Views.Workflow.Component
end
```

Then register it like any built-in view:

```elixir
views = [
  SelectoComponents.Views.spec(
    :workflow,
    SelectoComponentsWorkflow.Views.Workflow,
    "Workflow",
    %{drill_down: :detail}
  ),
  SelectoComponents.Views.spec(
    :faceted_product,
    SelectoComponentsFacetedProduct.Views.FacetedProduct,
    "Faceted Product",
    %{}
  )
]
```

Legacy namespace-style modules (`MyView.Process`, `MyView.Form`,
`MyView.Component`) are still supported.

### Implementing A New View System

Use this process for any new view package (for example
`selecto_components_view_workflow_inbox` or
`selecto_components_view_faceted_product`).

1. Create a package with name `selecto_components_view_<slug>`.
2. Add dependency on `selecto_components` (path dep for local/vendor, Hex dep for published use).
3. Implement a top-level view module that `use`s `SelectoComponents.Views.System`.
4. Implement the three modules referenced by the top-level view:
   `Process`, `Form`, `Component`.
5. Register the view in your LiveView `views` list with
   `SelectoComponents.Views.spec/4`.
6. Add the new view type to saved-view validation in your host app
   (if your app validates allowed view types).
7. Compile and test the LiveView by switching to the new tab and submitting.

Expected package layout:

```text
vendor/selecto_components_view_<slug>/
  mix.exs
  lib/selecto_components_view_<slug>.ex
  lib/selecto_components_view_<slug>/views/<slug>.ex
  lib/selecto_components_view_<slug>/views/<slug>/process.ex
  lib/selecto_components_view_<slug>/views/<slug>/form.ex
  lib/selecto_components_view_<slug>/views/<slug>/component.ex
```

Top-level view module:

```elixir
defmodule SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox do
  use SelectoComponents.Views.System,
    process: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Process,
    form: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Form,
    component: SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox.Component
end
```

`Process` callback contract:

```elixir
@callback initial_state(selecto :: term(), options :: map()) :: map()
@callback param_to_state(params :: map(), options :: map()) :: map()
@callback view(
  options :: map(),
  params :: map(),
  columns_map :: map(),
  filtered :: term(),
  selecto :: term()
) :: {view_set :: map(), view_meta :: map()}
```

Minimal registration in a LiveView:

```elixir
views = [
  SelectoComponents.Views.spec(:detail, SelectoComponents.Views.Detail, "Detail View", %{}),
  SelectoComponents.Views.spec(
    :workflow_inbox,
    SelectoComponentsViewWorkflowInbox.Views.WorkflowInbox,
    "Workflow Inbox",
    %{}
  )
]
```

If your app persists saved views by type, include your new type. Example:

```elixir
@view_types ~w(detail aggregate graph workflow_inbox faceted_product)
```

Verification checklist:

1. `mix compile` succeeds after adding deps and modules.
2. Open the LiveView, toggle View Controller, confirm your tab appears.
3. Select the tab, submit config, confirm results render.
4. Save and reload a saved view for the new type.
5. Confirm invalid/missing config shows a user-visible error state.

### Core Components
- `SelectoComponents.Components.TreeBuilder` - Drag-and-drop query builder with colocated JavaScript hook
- `SelectoComponents.Components.ListPicker` - Reorderable list selection component
- `SelectoComponents.Components.Tabs` - Tab navigation component
- `SelectoComponents.Components.RadioTabs` - Radio-style tab selection

### Support Modules
- `SelectoComponents.State` - State management for components
- `SelectoComponents.Router` - Event routing and business logic
- `SelectoComponents.Form` - Form handling utilities
- `SelectoComponents.Results` - Result processing and formatting

## JavaScript Hooks

SelectoComponents uses Phoenix LiveView's colocated JavaScript feature. The hooks are embedded directly in the components and extracted during compilation:

1. **`.TreeBuilder`** - Drag-and-drop functionality for the query builder
2. **`.GraphComponent`** - Interactive charting with Chart.js

These hooks are automatically available after running `mix selecto.components.integrate` or manually adding the import to your app.js.

## Troubleshooting

### Hooks Not Working

1. **Run the integration task**:
```bash
mix selecto.components.integrate --check  # Check if integrated
mix selecto.components.integrate          # Apply integration
```

2. **Verify app.js has the import**:
```javascript
import {hooks as selectoHooks} from "phoenix-colocated/selecto_components"
// ...
hooks: { ...selectoHooks }
```

3. **Rebuild assets**:
```bash
mix assets.build
```

4. **Check browser console** for JavaScript errors

### Styles Not Applied

1. **Verify app.css has the @source directive**:
```css
@source "../../deps/selecto_components/lib/**/*.{ex,heex}";
```

2. **Rebuild Tailwind**:
```bash
mix assets.build
```

### Integration Task Issues

If `mix selecto.components.integrate` fails:
- Check that `assets/js/app.js` and `assets/css/app.css` exist
- Use `--force` to re-apply integration: `mix selecto.components.integrate --force`
- Follow the manual setup steps above

## Development

This library is part of the Selecto ecosystem and is typically developed alongside:
- [selecto](https://github.com/selecto-elixir/selecto) - Core query building library
- [selecto_mix](https://github.com/selecto-elixir/selecto_mix) - Mix tasks and generators

## License

MIT License - see LICENSE file for details.