Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions config/kompass.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
use Secondnetwork\Kompass\Models\Datafield;
use Secondnetwork\Kompass\Models\File;
use Secondnetwork\Kompass\Models\Meta;
use Secondnetwork\Kompass\Models\Page;
use Secondnetwork\Kompass\Models\Post;

/*
| Shared building blocks for the block-type definitions below. Extracted to keep
| the registry DRY — the resolved config is identical to inlining these literals.
*/
$styleSlate = ['rail' => 'border-l-slate-400', 'badge' => 'bg-slate-500', 'bar' => 'bg-base-200', 'accent' => 'text-slate-500'];
$controlsBasic = ['layout', 'color', 'advanced'];
$controlsContainer = ['container-layout', 'layout-grid', 'color', 'advanced'];

return [

Expand Down Expand Up @@ -195,4 +205,199 @@
],
],

/*
|--------------------------------------------------------------------------
| Block Types (single source of truth)
|--------------------------------------------------------------------------
|
| Built-in block types consumed by Secondnetwork\Kompass\Blocks\BlockTypeRegistry.
| Each entry feeds the add-block palette, the default datafields created on
| add, the builder styling, the edit-control list and the frontend component
| name. User-defined Blocktemplates rows are merged in at runtime; built-ins
| win on a type collision. Available control keys map to the anonymous Blade
| components under resources/views/components/block-controls/*.
|
*/

'block_types' => [
'wysiwyg' => [
'label' => 'Textblock',
'icon' => 'blockquote',
'component' => 'blocks.wysiwyg',
'container' => false,
'default_fields' => [['type' => 'wysiwyg', 'order' => 1]],
'styling' => $styleSlate,
'controls' => ['layout', 'alignment', 'link', 'color', 'advanced'],
'palette' => true,
'palette_image' => 'icons-blocks/default.png',
'palette_border' => 'border-blue-600',
],
'group' => [
'label' => 'Layout Block',
'icon' => '',
'component' => 'blocks.group',
'container' => true,
'default_fields' => [],
'styling' => ['rail' => 'border-l-indigo-500', 'badge' => 'bg-indigo-500', 'bar' => 'bg-indigo-500/10', 'accent' => 'text-indigo-600'],
'controls' => $controlsContainer,
'palette' => true,
'palette_image' => 'icons-blocks/group.png',
'palette_border' => 'border-purple-600',
],
'accordiongroup' => [
'label' => 'Accordion',
'icon' => '',
'component' => 'blocks.accordiongroup',
'container' => true,
'default_fields' => [],
'styling' => ['rail' => 'border-l-emerald-500', 'badge' => 'bg-emerald-500', 'bar' => 'bg-emerald-500/10', 'accent' => 'text-emerald-600'],
'controls' => $controlsContainer,
'palette' => true,
'palette_image' => 'icons-blocks/accordiongroup.png',
'palette_border' => 'border-purple-600',
],
'button' => [
'label' => 'Button',
'icon' => 'box-model-2',
'component' => 'blocks.button',
'container' => false,
'default_fields' => [['type' => 'link', 'order' => 1]],
'styling' => $styleSlate,
'controls' => $controlsBasic,
'palette' => true,
'palette_image' => 'icons-blocks/button.png',
'palette_border' => 'border-blue-600',
],
'video' => [
'label' => 'Video',
'icon' => 'video',
'component' => 'blocks.video',
'container' => false,
'default_fields' => [],
'styling' => ['rail' => 'border-l-red-600', 'badge' => 'bg-slate-500', 'bar' => 'bg-slate-600/10', 'accent' => 'text-red-600'],
'controls' => $controlsBasic,
'palette' => true,
'palette_image' => 'icons-blocks/videoplayer.png',
'palette_border' => 'border-blue-600',
],
'gallery' => [
'label' => 'Images and Gallery',
'icon' => 'photo',
'component' => 'blocks.gallery',
'container' => false,
'default_fields' => [['type' => 'gallery', 'order' => 1, 'data' => []]],
'styling' => ['rail' => 'border-l-blue-500', 'badge' => 'bg-blue-500', 'bar' => 'bg-blue-500/10', 'accent' => 'text-blue-600'],
'controls' => ['layout', 'gallery', 'color', 'advanced'],
'palette' => true,
'palette_image' => 'icons-blocks/gallery.png',
'palette_image_class' => 'rounded',
'palette_border' => 'border-blue-600',
],
'anchormenu' => [
'label' => 'Anchor menu',
'icon' => '',
'component' => 'blocks.anchormenu',
'container' => false,
'default_fields' => [['name' => 'Name Anchormenu', 'type' => 'text', 'order' => 1]],
'styling' => $styleSlate,
'controls' => $controlsBasic,
'palette' => false,
],
'relationship' => [
'label' => 'Relationship',
'icon' => 'database',
'component' => 'blocks.relationship',
'container' => false,
'default_fields' => [],
'styling' => ['rail' => 'border-l-teal-500', 'badge' => 'bg-teal-500', 'bar' => 'bg-teal-500/10', 'accent' => 'text-teal-600'],
'controls' => $controlsBasic,
'palette' => true,
'palette_image' => 'icons-blocks/default.png',
'palette_border' => 'border-teal-600',
],
],

/*
|--------------------------------------------------------------------------
| Query Models (Relationship block)
|--------------------------------------------------------------------------
|
| Models the "relationship" block can query and list. Each entry registers
| a selectable source by key. The block stores its chosen source, ordering
| and limit in block meta (query-model, query-order, query-direction,
| query-limit) and renders the matched records via kompass_query().
|
| Sources are now database-managed (the `query_sources` table, edited under
| Admin → Query sources, seeded by QuerySourceSeeder). query_models() merges
| any entries defined here with the database rows — config wins on a key
| collision. Leave `query_models` empty to manage every source from the
| backend, or add a literal entry here to ship a hard-coded, non-editable
| source. Per-entry keys: label, model, label_field, order_fields,
| url_pattern, status, item_view, wrapper_class, with.
|
*/

/*
| Allowlist of models that database-defined query sources (the admin-managed
| `query_sources` table, merged in by query_models()) may be backed by. A
| source row stores one of these KEYS in `model_key`; the key is resolved to
| the class here. User input never supplies a raw class name, so an arbitrary
| class can never be instantiated. Add an entry to expose a model to the
| "create source" backend screen.
*/
'query_source_models' => [
'pages' => Page::class,
'posts' => Post::class,
],

'query_models' => [
// Managed in the database (Admin → Query sources). See QuerySourceSeeder
// for the default Pages / Blog posts sources.
],

/*
|--------------------------------------------------------------------------
| Field Types (datafields)
|--------------------------------------------------------------------------
|
| Datafield types consumed by Secondnetwork\Kompass\Blocks\FieldTypeRegistry.
| display_component = anonymous Blade component (under kompass::) used to
| render the saved value in the builder; edit_widget = the interactive editor
| widget (input|image|oembed|editor); select=false hides it from the
| field-type picker.
|
*/

'field_types' => [
'text' => ['label' => 'Text', 'icon' => 'tabler-letter-case', 'display_component' => 'block.text', 'edit_widget' => 'input'],
'wysiwyg' => ['label' => 'WYSIWYG Editor', 'icon' => 'tabler-blockquote', 'display_component' => 'block.wysiwyg', 'edit_widget' => 'editor'],
'image' => ['label' => 'Image', 'icon' => 'tabler-photo', 'display_component' => 'block.image', 'edit_widget' => 'image'],
'gallery' => ['label' => 'Gallery', 'icon' => 'tabler-layout-grid-add', 'display_component' => 'block.gallery-field', 'edit_widget' => 'gallery'],
'link' => ['label' => 'Link', 'icon' => 'tabler-link', 'display_component' => 'block.link', 'edit_widget' => 'input'],
'true_false' => ['label' => 'true/false', 'icon' => 'tabler-toggle-left', 'display_component' => 'block.true_false', 'edit_widget' => 'input'],
'file' => ['label' => 'File', 'icon' => 'tabler-file-zip', 'display_component' => 'block.file', 'edit_widget' => 'input'],
'color' => ['label' => 'Color', 'icon' => 'tabler-palette', 'display_component' => 'block.color', 'edit_widget' => 'input'],
'oembed' => ['label' => 'Video embed', 'icon' => 'tabler-brand-youtube', 'display_component' => 'block.text', 'edit_widget' => 'oembed', 'select' => false],
],

/*
|--------------------------------------------------------------------------
| Setting Field Types
|--------------------------------------------------------------------------
|
| Distinct vocabulary used by the global settings field-type picker
| (components/elements/global.blade.php). Kept separate from field_types
| because its ids (rich_text_box, switch) drive a different render switch.
|
*/

'setting_field_types' => [
'text' => ['label' => 'Text', 'icon' => 'tabler-letter-case'],
'wysiwyg' => ['label' => 'WYSIWYG Editor', 'icon' => 'tabler-blockquote'],
'image' => ['label' => 'Image', 'icon' => 'tabler-photo'],
'link' => ['label' => 'Link', 'icon' => 'tabler-link'],
'switch' => ['label' => 'true or false', 'icon' => 'tabler-toggle-left'],
'file' => ['label' => 'File', 'icon' => 'tabler-file-zip'],
],

];
169 changes: 169 additions & 0 deletions docs/query-sources-db-concept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Concept: Database-managed query sources (Relationship block)

> **Status: proposal / not implemented.** This document describes how the
> `query_models` registry (see [relationship-block.md](relationship-block.md))
> could be made manageable from the backend / database, including the hard
> constraints and a recommended architecture. No code exists yet.

## Goal

Today, relationship-block sources live in `config/kompass.php` → `query_models`.
Adding a source means editing a PHP file. The goal is to let an administrator
**register and configure sources from the admin UI**, stored in the database —
mirroring how `Blocktemplates` already extends the built-in `block_types`.

## The hard constraint: data vs. code

A `query_models` entry mixes two fundamentally different things:

| Part | DB-manageable? | Reason |
| --- | --- | --- |
| `label`, `label_field`, `order_fields`, `url_pattern`, `status`, `wrapper_class`, `with` | ✅ yes | plain configuration values (strings / arrays) |
| `model` (e.g. `App\Models\TeamMember`) | ⚠️ allowlist only | a PHP/Eloquent class — must already exist in code |
| `item_view` (e.g. `relations.team`) | ⚠️ selection only | a Blade component — must already exist as a file |

**Conclusion:** the backend can *wire up* which existing model + existing view
are offered as a source, and set all the plain config values. It **cannot create
a new model or a new layout from the database** — a model is a table + Eloquent
class, an item view is a Blade template; both are code.

## Security: never trust a class name from the DB

`kompass_query()` does `$modelClass::query()`. If `$modelClass` came from a free
text field stored in the DB, that is an arbitrary-class-instantiation risk.

**Rule:** the model must be chosen from a server-side **allowlist** registered in
config — never a free-text class name. Proposed config key:

```php
// config/kompass.php
'query_source_models' => [
// key => FQCN; only these may back a DB source
'pages' => \Secondnetwork\Kompass\Models\Page::class,
'posts' => \Secondnetwork\Kompass\Models\Post::class,
'team' => \App\Models\TeamMember::class,
],
```

The admin form offers a `<select>` of these keys; the DB stores the **key**, and
the merge layer resolves it to the FQCN. An unknown key resolves to nothing
(source is skipped), so stale rows can never instantiate an arbitrary class.

## Proposed schema: `query_sources`

```php
Schema::create('query_sources', function (Blueprint $table) {
$table->id();
$table->string('key')->unique(); // e.g. "team" (used as query-model meta value)
$table->string('label');
$table->string('model_key'); // FK into config('kompass.query_source_models')
$table->string('label_field')->default('title');
$table->json('order_fields')->nullable();
$table->string('url_pattern')->nullable();
$table->string('status_filter')->nullable();
$table->string('item_view')->nullable(); // chosen from scanned components/relations/*
$table->string('wrapper_class')->nullable();
$table->json('with')->nullable();
$table->integer('order')->default(0);
$table->timestamps();
});
```

## Merge layer

`query_models()` changes from "config only" to "config + DB", following the exact
pattern `BlockTypeRegistry` uses for built-ins + `Blocktemplates`:

```php
function query_models(): array
{
$config = config('kompass.query_models', []);

// Cache to avoid a query on every helper call within a request.
$db = cache()->rememberForever('kompass-query-sources', function () {
$allow = config('kompass.query_source_models', []);

return QuerySource::orderBy('order')->get()
->filter(fn ($s) => isset($allow[$s->model_key])) // allowlist guard
->mapWithKeys(fn ($s) => [$s->key => [
'label' => $s->label,
'model' => $allow[$s->model_key], // resolve key -> FQCN
'label_field' => $s->label_field,
'order_fields' => $s->order_fields ?? ['created_at'],
'url_pattern' => $s->url_pattern,
'status' => $s->status_filter,
'item_view' => $s->item_view,
'wrapper_class' => $s->wrapper_class,
'with' => $s->with ?? [],
]])
->all();
});

// Config wins on key collision (built-ins are never shadowed by DB rows).
return $db + $config;
}
```

The cache must be flushed on `QuerySource` create/update/delete (the same
`cache()->flush()` hook the `Block` model already uses).

**No change needed** in `kompass_query()`, `kompass_query_candidates()`,
`kompass_query_url()` or the block UI — they all read through `query_models()`,
so DB sources work everywhere automatically.

## Admin UI

- A Livewire CRUD screen (e.g. `QuerySources`) under `/admin/...`, registered in
the **admin-only** route group (same group `/admin/blocks` now lives in).
- Form fields:
- **Key** — slug, unique (validated; this becomes the `query-model` meta value).
- **Label** — free text.
- **Model** — `<select>` from `config('kompass.query_source_models')` (allowlist).
- **Label field / order fields** — text inputs; could be auto-suggested from the
model's `Schema::getColumnListing()`.
- **URL pattern**, **status filter**, **wrapper class** — text.
- **Item view** — `<select>` populated by scanning
`resources/views/components/relations/*.blade.php` (existing files only).
- **Eager loads (`with`)** — repeater of relation names.
- Validation: `key` unique & slug; `model_key` ∈ allowlist; `item_view` ∈ scanned
views; `order_fields` ⊆ the model's columns.

## Migration / backward compatibility

- Existing config sources keep working unchanged (config wins on collision).
- DB sources are additive — removing the feature later just stops merging them.
- Editors' saved blocks reference a source by its `key` in block meta
(`query-model`); a deleted source ⇒ `query_models()[$key]` is missing ⇒
`kompass_query()` already returns an empty collection (graceful).

## Optional stage B: field-mapping renderer

To remove the "item view must be a code file" constraint for standard cases:

- Add a `field_map` JSON column to `query_sources` (e.g.
`{ "title": "name", "image": "photo", "text": "description" }`).
- Provide one generic frontend view `relations/_generic.blade.php` that
renders title / image / text / link from the mapped columns.
- A source with no custom `item_view` falls back to the generic renderer driven
by `field_map`.
- Custom Blade item views remain available for bespoke layouts.

This lets admins build a "team grid" or "post list" entirely from the DB, while
complex/branded layouts still use a hand-written component.

## Open questions

1. Should sources be **per-locale** (e.g. different label per language)?
2. Should the model allowlist be **auto-discovered** (scan `app/Models`) or stay
an explicit config list? (Explicit is safer.)
3. Where exactly should the admin screen live in the navigation — under "Blocks"
or a new "Content sources" entry?
4. Is stage B (field mapping) wanted now, or is stage A (wiring) enough?

## Effort estimate

- **Stage A (wiring):** migration + `QuerySource` model + cache-aware merge in
`query_models()` + one Livewire admin CRUD screen + route + nav link +
validation. Moderate.
- **Stage B (field mapping):** + `field_map` column, generic renderer view, and
the field-mapping UI. Larger.
Loading
Loading