Skip to content
Draft
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
2 changes: 1 addition & 1 deletion resources/dist/flowforge.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions resources/js/flowforge.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export default function flowforge({state}) {
state,
isLoading: {},
fullyLoaded: {},
collapsedSwimlanes: {},

init() {
this.$wire.$on('kanban-items-loaded', (event) => {
Expand All @@ -11,6 +12,11 @@ export default function flowforge({state}) {
this.fullyLoaded[columnId] = true;
}
});

// Restore collapsed swimlane state from localStorage
if (this.state.swimlanes) {
this._restoreSwimlaneState();
}
},

handleSortableEnd(event) {
Expand Down Expand Up @@ -80,5 +86,43 @@ export default function flowforge({state}) {
this.handleSmoothScroll(columnId);
}
},

// --- Swimlane collapse/expand ---

toggleSwimlane(swimlaneId) {
this.collapsedSwimlanes[swimlaneId] = !this.collapsedSwimlanes[swimlaneId];
this._saveSwimlaneState();
},

isSwimlaneCollapsed(swimlaneId) {
return this.collapsedSwimlanes[swimlaneId] || false;
},

_getSwimlaneStorageKey() {
// Use the page URL path as a board-specific key
return 'flowforge:swimlanes:' + window.location.pathname;
},

_saveSwimlaneState() {
try {
localStorage.setItem(
this._getSwimlaneStorageKey(),
JSON.stringify(this.collapsedSwimlanes)
);
} catch (e) {
// localStorage may be unavailable; ignore silently
}
},

_restoreSwimlaneState() {
try {
const stored = localStorage.getItem(this._getSwimlaneStorageKey());
if (stored) {
this.collapsedSwimlanes = JSON.parse(stored);
}
} catch (e) {
this.collapsedSwimlanes = {};
}
},
}
}
1 change: 1 addition & 0 deletions resources/lang/en/flowforge.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
'plural_card_label' => 'Records',
'cards_pagination' => ':current of :total :cards',
'all_cards_loaded' => 'All :total :cards loaded',
'uncategorized' => 'Uncategorized',
];
36 changes: 24 additions & 12 deletions resources/views/index.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@php use Filament\Support\Facades\FilamentAsset; @endphp
@props(['columns', 'config'])
@props(['columns', 'swimlanes', 'config'])

<div
class="w-full h-full flex flex-col relative"
Expand All @@ -8,8 +8,10 @@ class="w-full h-full flex flex-col relative"
x-data="flowforge({
state: {
columns: @js($columns),
swimlanes: @js($swimlanes),
titleField: '{{ $config['recordTitleAttribute'] }}',
columnField: '{{ $config['columnIdentifierAttribute'] }}',
swimlaneField: '{{ $config['swimlaneIdentifierAttribute'] ?? '' }}',
cardLabel: '{{ $config['cardLabel'] }}',
pluralCardLabel: '{{ $config['pluralCardLabel'] }}',
}
Expand All @@ -19,18 +21,28 @@ class="w-full h-full flex flex-col relative"
@include('flowforge::components.filters')

<!-- Board Content -->
<div class="flex-1 overflow-hidden h-full">
<div class="flex flex-row h-full overflow-x-auto overflow-y-hidden gap-5">
@foreach($columns as $columnId => $column)
<x-flowforge::column
:columnId="$columnId"
:column="$column"
:config="$config"
wire:key="column-{{ $columnId }}"
/>
@endforeach
@if($swimlanes)
{{-- 2D Swimlane Grid Layout --}}
<x-flowforge::swimlane-board
:columns="$columns"
:swimlanes="$swimlanes"
:config="$config"
/>
@else
{{-- Flat Column Layout (original) --}}
<div class="flex-1 overflow-hidden h-full">
<div class="flex flex-row h-full overflow-x-auto overflow-y-hidden gap-5">
@foreach($columns as $columnId => $column)
<x-flowforge::column
:columnId="$columnId"
:column="$column"
:config="$config"
wire:key="column-{{ $columnId }}"
/>
@endforeach
</div>
</div>
</div>
@endif

<x-filament-actions::modals/>
</div>
69 changes: 48 additions & 21 deletions resources/views/livewire/card.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<div
@class([
'flowforge-card mb-3 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden transition-all duration-200 hover:shadow-md',
'flowforge-card mb-2 bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden transition-all duration-200 hover:shadow-md max-w-[300px]',
'cursor-pointer' => $hasActions || $hasCardAction,
'cursor-pointer transition-all duration-100 ease-in-out hover:shadow-lg hover:border-gray-400 active:shadow-md' => $hasCardAction,
'cursor-grab hover:cursor-grabbing' => $hasPositionIdentifier,
Expand All @@ -23,34 +23,61 @@
@endif
data-position="{{ $record['position'] ?? '' }}"
>
<div class="flowforge-card-content">
<div class="flex items-start justify-between mb-2">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white p-3"
@if($hasCardAction && $cardAction)
wire:click="mountAction('{{ $cardAction }}', [], @js(['recordKey' => $record['id']]))"
style="cursor: pointer;"
<div class="flowforge-card-content px-3 pt-2 pb-2"
@if($hasCardAction && $cardAction)
wire:click="mountAction('{{ $cardAction }}', [], @js(['recordKey' => $record['id']]))"
style="cursor: pointer;"
@endif
>
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
{{-- Optional header (e.g. ticket number) --}}
@if(filled($record['headerSchema'] ?? null))
<div class="flowforge-card-schema">
{{ $record['headerSchema'] }}
</div>
@endif
>
{{ $record['title'] }}
</h4>

{{-- Card title --}}
<h4 class="text-xs font-semibold text-gray-900 dark:text-white leading-snug">
{{ $record['title'] }}
</h4>
</div>

@if($hasActions)
<div class="m-3">
<div class="flex-shrink-0 -mt-0.5 -mr-1">
<x-filament-actions::group :actions="$processedRecordActions"/>
</div>
@endif
</div>

<div class="px-3 pb-3"
@if($hasCardAction && $cardAction)
wire:click="mountAction('{{ $cardAction }}', [], @js(['recordKey' => $record['id']]))"
style="cursor: pointer;"
@endif
>
{{-- Render card schema with compact spacing --}}
@if(filled($record['schema']))
{{-- Card schema (body fields) --}}
@if(filled($record['schema']))
<div class="flowforge-card-schema">
{{ $record['schema'] }}
@endif
</div>
</div>
@endif
</div>

{{-- Optional card footer --}}
@if(filled($record['footerSchema'] ?? null))
<div class="flowforge-card-footer border-t border-gray-200 dark:border-gray-700 px-3 py-1.5">
<div class="flowforge-card-schema">
{{ $record['footerSchema'] }}
</div>
</div>
@endif
</div>

@once
<style>
/* Collapse Filament's default gap-6 between schema entries inside cards */
.flowforge-card-schema .fi-sc.fi-sc-has-gap {
gap: 0;
}
/* Remove internal gap within each entry wrapper */
.flowforge-card-schema .fi-in-entry {
gap: 0;
}
</style>
@endonce
146 changes: 146 additions & 0 deletions resources/views/livewire/swimlane-board.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
@php use Relaticle\Flowforge\Support\ColorResolver; @endphp
@props(['columns', 'swimlanes', 'config'])

@php
$columnIds = array_keys($columns);
$columnCount = count($columnIds);
$hasPositionIdentifier = $this->getBoard()->getPositionIdentifierAttribute() !== null;
@endphp

<div
class="flowforge-swimlane-board overflow-x-auto rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm"
x-data="{
collapsed: {},
init() {
try {
const stored = localStorage.getItem('flowforge:swimlanes:' + window.location.pathname);
if (stored) this.collapsed = JSON.parse(stored);
} catch(e) { this.collapsed = {}; }
},
toggle(id) {
this.collapsed[id] = !(this.collapsed[id] || false);
try { localStorage.setItem('flowforge:swimlanes:' + window.location.pathname, JSON.stringify(this.collapsed)); } catch(e) {}
},
isCollapsed(id) {
return this.collapsed[id] || false;
}
}"
>
<table class="border-collapse w-full" style="min-width: {{ 180 + ($columnCount * 300) }}px;">
{{-- Sticky Column Header Row --}}
<thead>
<tr>
{{-- Empty corner cell --}}
<th class="sticky top-0 left-0 z-30 bg-white dark:bg-gray-900 border-b border-r border-gray-200 dark:border-gray-700 p-2" style="width: 180px; min-width: 180px;"></th>

@foreach($columns as $columnId => $column)
@php
$resolvedColor = ColorResolver::resolve($column['color']);
$isSemantic = ColorResolver::isSemantic($resolvedColor);
@endphp
<th
class="sticky top-0 z-20 bg-white dark:bg-gray-900 border-b border-r border-gray-200 dark:border-gray-700 p-3 text-left font-normal"
style="min-width: 300px;"
wire:key="header-{{ $columnId }}"
>
<div class="flex items-center">
@if ($column['icon'] ?? null)
<x-filament::icon :icon="$column['icon']" class="h-4 w-4 text-gray-500 dark:text-gray-400 me-2" />
@endif
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-200 truncate">
{{ $column['label'] }}
</h3>
</div>
</th>
@endforeach
</tr>
</thead>

<tbody>
@foreach($swimlanes as $swimlaneId => $swimlane)
@php
$resolvedLaneColor = ColorResolver::resolve($swimlane['color']);
$isLaneSemantic = ColorResolver::isSemantic($resolvedLaneColor);
$laneColorShades = $isLaneSemantic ? null : $resolvedLaneColor;
@endphp

{{-- Swimlane Row --}}
<tr wire:key="lane-row-{{ $swimlaneId }}">
{{-- Swimlane Label Cell (row header) --}}
<td
class="sticky left-0 z-10 bg-white dark:bg-gray-900 border-b border-r border-gray-200 dark:border-gray-700 p-3"
style="width: 180px; min-width: 180px; vertical-align: top;"
wire:key="lane-label-{{ $swimlaneId }}"
>
<button
type="button"
class="flex items-center gap-2 w-full text-left group"
x-on:click="toggle('{{ $swimlaneId }}')"
>
{{-- Collapse/expand chevron --}}
<svg
class="w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 flex-shrink-0"
:class="{ '-rotate-90': isCollapsed('{{ $swimlaneId }}') }"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
>
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>

<span class="text-sm font-semibold text-gray-700 dark:text-gray-200 truncate">
{{ $swimlane['label'] }}
</span>

{{-- Count badge --}}
@if($isLaneSemantic)
<x-filament::badge tag="div" :color="$resolvedLaneColor" size="sm">
{{ $swimlane['total'] }}
</x-filament::badge>
@elseif($laneColorShades)
<div
@style([
Filament\Support\get_color_css_variables($resolvedLaneColor, shades: [50, 300, 600, 700])
])
@class([
'items-center border px-1.5 py-0.5 rounded-md text-xs font-semibold',
'bg-custom-50 dark:bg-custom-600/20',
'text-custom-700 dark:text-custom-300',
'border-custom-700/30 dark:border-custom-300/30',
])
>
{{ $swimlane['total'] }}
</div>
@else
<div class="items-center border px-1.5 py-0.5 rounded-md text-xs font-semibold bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600">
{{ $swimlane['total'] }}
</div>
@endif
</button>
</td>

{{-- Swimlane Cells --}}
@foreach($columnIds as $columnId)
@php
$cell = $swimlane['cells'][$columnId] ?? ['items' => [], 'total' => 0];
$cellKey = $columnId . '|' . $swimlaneId;
@endphp
<td
class="border-b border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-0"
style="min-width: 300px; vertical-align: top;"
wire:key="cell-{{ $cellKey }}"
>
<div x-show="!isCollapsed('{{ $swimlaneId }}')">
<x-flowforge::swimlane-cell
:columnId="$columnId"
:swimlaneId="$swimlaneId"
:cell="$cell"
:config="$config"
:hasPositionIdentifier="$hasPositionIdentifier"
/>
</div>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
Loading