coopdevs / coopdevs/timeoverflow

Replace ActiveAdmin with custom admin controllers

Open
#837 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
Ruby
Stars
170
Forks
75
PR merge metrics
No merged PRs in 30d

Description

## Context

Part of #835 (supply chain risk reduction). ActiveAdmin is a large, opaque framework that generates behaviour through a proprietary DSL — hard to audit, hard to extend outside its abstractions, and brings several transitive dependencies (`activeadmin`, `has_scope`, `select2-rails`). The goal is to replace it with a custom admin section that:

- Lives entirely in the project (an internalised DSL)
- Uses only gems already present (Bootstrap 5, simple_form, kaminari, devise, pundit)
- Provides enough shared structure to avoid repetition across the 8 resources
- Opens a clean path to progressive enhancement with Stimulus/Turbo Frames later

The approach: a namespaced controller hierarchy (`Admin::`) with shared partials as the visual building blocks, keeping all logic readable and grep-able without knowing a framework.

---

## Existing gems that already cover all needs (nothing new required)

| Need | Gem | Status |
|------|-----|--------|
| Styling | `bootstrap ~> 5.3.3` | ✅ already in Gemfile |
| Forms | `simple_form ~> 5.0.2` | ✅ already in Gemfile |
| Pagination | `kaminari ~> 1.2.1` | ✅ already in Gemfile |
| Auth | `devise ~> 4.9.1` | ✅ already in Gemfile |
| Authz | `pundit ~> 2.1.0` | ✅ already in Gemfile |
| CSV import | existing service objects | ✅ already exist |

Gems to **remove** after migration: `activeadmin ~> 3.2`, `has_scope ~> 0.7.2`, `select2-rails ~> 4.0.13`

---

## Architecture overview

### File layout

```
app/
controllers/
admin/
base_controller.rb # auth, layout, shared helpers
concerns/
csv_importable.rb # shared upload_csv / import_csv actions
dashboard_controller.rb
users_controller.rb
organizations_controller.rb
posts_controller.rb
categories_controller.rb
transfers_controller.rb
petitions_controller.rb
documents_controller.rb
views/
admin/
shared/
_page_header.html.erb # title + action buttons
_filters.html.erb # filter form wrapper (Bootstrap card)
_flash.html.erb # alert/notice banners
_pagination.html.erb # kaminari links
_csv_upload_form.html.erb # shared CSV upload form
layouts/admin.html.erb # Bootstrap 5 navbar + main content
dashboard/index.html.erb
users/ organizations/ posts/ categories/ transfers/ petitions/ documents/
assets/
stylesheets/admin.scss # brand colour + CSS View Transition rules
```

### Base controller

```ruby
class Admin::BaseController < ApplicationController
layout 'admin'
before_action :authenticate_superadmin!

private

def authenticate_superadmin!
authenticate_user!
redirect_to root_path, alert: t('not_authorized') unless current_user.superadmin?
end
end
```

Reuses existing `superadmin?` from `config/initializers/superadmins.rb`.

### The internalised DSL — three parts

**1. A concern for CSV import** (`Admin::CsvImportable`)
Provides `upload_csv` / `import_csv` actions, wired with one line:
```ruby
class Admin::UsersController < Admin::BaseController
include Admin::CsvImportable
csv_importer UserImporter # existing service object
end
```

**2. Inline filtering** — plain ActiveRecord scopes + `params`, no Ransack (keeps dependency count down):
```ruby
def index
scope = User.includes(:members)
scope = scope.where('email LIKE ?', "%#{params[:email]}%") if params[:email].present?
scope = scope.where(locale: params[:locale]) if params[:locale].present?
@users = scope.order(created_at: :desc).page(params[:page])
end
```

**3. Shared partials as visual DSL** — every index view reads the same way:
```erb
<%= render 'admin/shared/page_header', title: 'Users', new_path: new_admin_user_path %>
<%= render 'admin/users/filters' %>

<%= render 'admin/shared/pagination', collection: @users %>
```
Plain ERB, no magic, fully grep-able.

### Locality of behaviour — modern browser APIs first

Since we can target modern browsers:

- **Native ``** for confirmation modals — no Bootstrap modal JS needed
- **Native Popover API** (`popover` + `popovertarget`) for filter panels and action menus — zero JS
- **CSS View Transitions** for smooth page-to-page navigation — zero JS:

```scss
@view-transition { navigation: auto; }
::view-transition-old(root) { animation: 120ms ease-out fade-out; }
::view-transition-new(root) { animation: 120ms ease-in fade-in; }
```

Bootstrap 5 JS bundle can therefore be skipped for the admin layout; only Bootstrap CSS is needed.

A Stimulus layer (live filter debounce, in-place toggles, Turbo Frames around results) can be added as a follow-up without changing the architecture.

---

## Resources to migrate

| Resource | Actions | Notes |
|----------|---------|-------|
| Dashboard | `index` | Stats queries, recent items |
| User | full CRUD + CSV | Nested memberships form, `without_memberships` scope |
| Organization | full CRUD | Logo upload (Active Storage), guarded destroy |
| Post | full CRUD + CSV | Offer/Inquiry subclass selector |
| Category | full CRUD | Icon name field, translations display |
| Transfer | `index`, `destroy` + CSV | `seconds_to_hm` formatted amount |
| Petition | `index`, `destroy` | Guarded destroy (cannot delete accepted petitions) |
| Document | full CRUD | Per-locale title + content fields |

---

## Routes

```ruby
namespace :admin do
root to: 'dashboard#index'
resources :users do
collection { get :upload_csv; post :import_csv }
end
resources :organizations
resources :posts do
collection { get :upload_csv; post :import_csv }
end
resources :categories
resources :transfers, only: [:index, :destroy] do
collection { get :upload_csv; post :import_csv }
end
resources :petitions, only: [:index, :destroy]
resources :documents
end
```

---

## Migration phases

- **Phase 1** — Infrastructure: `BaseController`, layout, `admin.scss`, routes skeleton
- **Phase 2** — Simple resources: Category, Document, Petition, Transfer
- **Phase 3** — Medium resources: Post (CSV + subclass), Organization (file upload, guarded destroy)
- **Phase 4** — Complex resource: User (nested memberships, scopes, batch destroy, CSV)
- **Phase 5** — Dashboard (stats + recent items)
- **Phase 6** — `Admin::CsvImportable` concern + wire up existing importers
- **Phase 7** — Remove `activeadmin`, `has_scope`, `select2-rails` from Gemfile; delete `app/admin/`; clean up locale files

---

## Verification

After each phase:
- `rails routes | grep admin` — expected routes present
- Browser: superadmin can access, non-superadmin is redirected

After Phase 7:
- `bundle exec rails assets:precompile` succeeds
- Test suite passes
- `grep -r 'active_admin\|ActiveAdmin' app/ config/` returns nothing

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.