defaultSort('stars', 'desc') ->columns([ TextColumn::make('full_name') ->label(__('panel.repositories.fields.full_name')) ->weight('medium') ->description(fn (Repository $r) => $r->description) ->wrap() ->url(fn (Repository $r) => $r->html_url, shouldOpenInNewTab: true) ->searchable() ->sortable(), TextColumn::make('project.title') ->label(__('panel.repositories.fields.project')) ->badge() ->color('gray') ->placeholder('—') ->toggleable(), TextColumn::make('primary_language') ->label(__('panel.repositories.fields.language')) ->badge() ->placeholder('—'), TextColumn::make('stars') ->label('★') ->badge() ->color('warning') ->sortable(), TextColumn::make('contributors_count') ->label(__('panel.repositories.fields.contributors')) ->badge() ->color('gray') ->sortable(), TextColumn::make('good_first_issues_count') ->label(__('panel.repositories.fields.good_first_issues')) ->counts('goodFirstIssues') ->badge() ->color(fn ($state) => $state > 0 ? 'success' : 'gray'), TextColumn::make('pushed_at') ->label(__('panel.repositories.fields.pushed_at')) ->since() // A repository nobody has touched in months is worth // spotting: it is what makes a site look abandoned. ->color(fn (Repository $r) => $r->isDormant() ? 'danger' : null) ->placeholder('—') ->sortable(), IconColumn::make('show_publicly') ->label(__('panel.repositories.fields.show_publicly')) ->boolean() ->toggleable(), TextColumn::make('last_synced_at') ->label(__('panel.repositories.fields.last_synced_at')) ->since() ->description(fn (Repository $r) => $r->sync_error ? str($r->sync_error)->limit(60)->toString() : null) ->color(fn (Repository $r) => $r->sync_error ? 'danger' : 'gray') ->placeholder(__('panel.repositories.never_synced')), ]) ->filters([ TernaryFilter::make('show_publicly') ->label(__('panel.repositories.fields.show_publicly')), Filter::make('failing') ->label(__('panel.repositories.filters.failing')) ->query(fn (Builder $q) => $q->failing()), Filter::make('dormant') ->label(__('panel.repositories.filters.dormant')) ->query(fn (Builder $q) => $q->where(fn (Builder $sub) => $sub ->whereNull('pushed_at') ->orWhere('pushed_at', '<', now()->subDays(180)))), ]) ->recordActions([ self::syncAction(), EditAction::make(), DeleteAction::make(), ]) ->toolbarActions([ BulkActionGroup::make([ self::syncBulkAction(), DeleteBulkAction::make(), ]), ]); } /** * Runs inline rather than queueing, so the person who clicked sees the * result — including the reason when it fails. */ protected static function syncAction(): Action { return Action::make('sync') ->label(__('panel.repositories.actions.sync')) ->icon('heroicon-o-arrow-path') ->color('gray') ->visible(fn () => Auth::user()?->can('repositories.sync')) ->action(function (Repository $record, GitManager $git): void { try { $record = $git->sync($record); Notification::make() ->success() ->title(__('panel.repositories.notifications.synced')) ->body(__('panel.repositories.notifications.synced_body', [ 'stars' => $record->stars, 'issues' => $record->goodFirstIssues()->count(), ])) ->send(); } catch (GitException $e) { Notification::make() ->danger() ->title(__('panel.repositories.notifications.sync_failed')) ->body($e->getMessage()) ->persistent() ->send(); } }); } /** * Syncing many repositories at once can exhaust the provider's rate limit, * so this one goes through the queue instead of blocking the request. */ protected static function syncBulkAction(): BulkAction { return BulkAction::make('syncAll') ->label(__('panel.repositories.actions.sync_selected')) ->icon('heroicon-o-arrow-path') ->requiresConfirmation() ->visible(fn () => Auth::user()?->can('repositories.sync')) ->action(function (Collection $records): void { $records->each(fn (Repository $r) => SyncRepository::dispatch($r)); Notification::make() ->success() ->title(__('panel.repositories.notifications.queued', ['count' => $records->count()])) ->send(); }) ->deselectRecordsAfterCompletion(); } }