Skip to content
Open
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
49 changes: 49 additions & 0 deletions app/Actions/UpdateOrganizationProfile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace App\Actions;

use App\Contracts\Services\GitHubProvider;
use App\Models\Organization;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Client\HttpClientException;

/**
* Refreshes an organization's public GitHub profile without changing its members or chosen color.
*/
final readonly class UpdateOrganizationProfile
{
public function __construct(
private GitHubProvider $github,
private UpdateOrCreateOrganization $organizations,
) {
//
}

public function handle(Organization $organization): bool
{
try {
$githubId = $organization->profile->github_id;
$account = $this->github->account($githubId);

// Never follow a renamed handle to a different account, or turn a user into an organization.
if ($account === null
|| ($account['type'] ?? null) !== 'Organization'
|| ! is_int($account['id'] ?? null)
|| (string) $account['id'] !== $githubId
|| ! is_string($account['login'] ?? null)
|| $account['login'] === '') {
return false;
}

$updated = $this->organizations->handle($account);

return $updated->is($organization);
} catch (HttpClientException|UniqueConstraintViolationException $exception) {
report($exception);

return false;
}
}
}
18 changes: 18 additions & 0 deletions app/Http/Controllers/OrganizationProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace App\Http\Controllers;

use App\Actions\UpdateOrganizationProfile;
use App\Enums\OrganizationRole;
use App\Http\Requests\ShowOrganizationProfileRequest;
use App\Models\Organization;
use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;

Expand All @@ -20,6 +22,22 @@ public function edit(ShowOrganizationProfileRequest $request, Organization $orga
return Inertia::render('organization-profile/edit', [
'organization' => $organization,
'role' => OrganizationRole::Admin,
'githubSyncedAt' => $organization->github_synced_at?->toIso8601String(),
]);
}

public function update(ShowOrganizationProfileRequest $request, Organization $organization, UpdateOrganizationProfile $action): RedirectResponse
{
$synced = $action->handle($organization);

Inertia::flash('toast', $synced ? [
'type' => 'success',
'message' => 'Synced from GitHub.',
] : [
'type' => 'error',
'message' => 'Could not sync from GitHub. Try again in a minute.',
]);

return back();
}
}
107 changes: 105 additions & 2 deletions resources/js/pages/organization-profile/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,128 @@
import { Form, Head, router } from '@inertiajs/react';
import { ArrowUpRight, RefreshCw } from 'lucide-react';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import OrganizationLaneController from '@/actions/App/Http/Controllers/OrganizationLaneController';
import OrganizationProfileController from '@/actions/App/Http/Controllers/OrganizationProfileController';
import Heading from '@/components/heading';
import InputError from '@/components/input-error';
import { LaneOptions } from '@/components/lane-picker';
import { OrganizationAvatar } from '@/components/organization-avatar';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { useNow } from '@/hooks/use-now';
import AppLayout from '@/layouts/app-layout';
import OrganizationSettingsLayout from '@/layouts/organization-settings/layout';
import { organizationLaneColor, withViewTransition } from '@/lib/utils';
import {
formatFullDateTime,
formatSyncedAgo,
organizationLaneColor,
withViewTransition,
} from '@/lib/utils';
import type { Lane, Organization } from '@/types';

export default function Edit({ organization }: { organization: Organization }) {
export default function Edit({
organization,
githubSyncedAt,
}: {
organization: Organization;
githubSyncedAt: string | null;
}) {
const { lane } = organization;
const now = useNow();
const [selected, setSelected] = useState<Lane | null>(lane);

return (
<>
<Head title={`Profile of @${organization.handle}`} />

<section className="px-4 py-6 sm:px-6">
<Heading
title="Profile information"
description="The organization's public profile is mirrored from GitHub. Edit it there, then sync to update it here."
/>

<dl className="mt-5 divide-y rounded-lg border text-sm">
{[
{ label: 'name', value: organization.name },
{ label: 'username', value: `@${organization.handle}` },
{ label: 'bio', value: organization.bio },
{ label: 'location', value: organization.location },
{ label: 'website', value: organization.website },
].map((field) => (
<div
key={field.label}
className="grid grid-cols-[6.5rem_minmax(0,1fr)] gap-3 px-4 py-2.5"
>
<dt className="font-mono text-[13px] text-muted-foreground">
{field.label}
</dt>
<dd
className={
field.value
? 'wrap-break-word'
: 'font-mono text-[13px] text-muted-foreground'
}
>
{field.value ?? 'null'}
</dd>
</div>
))}
</dl>

<div className="mt-5 flex flex-wrap gap-2">
<Button asChild>
<a
href={`https://github.com/organizations/${organization.handle}/settings/profile`}
target="_blank"
rel="noopener noreferrer"
>
Edit on GitHub
<ArrowUpRight />
</a>
</Button>
<Form
{...OrganizationProfileController.update.form(
organization.id,
)}
options={{ preserveScroll: true }}
onSuccess={() => router.flushAll()}
>
{({ processing }) => (
<Button
type="submit"
variant="outline"
disabled={processing}
aria-busy={processing}
data-test="sync-organization-profile-button"
>
{processing ? <Spinner /> : <RefreshCw />}
Sync from GitHub
</Button>
)}
</Form>
</div>

<p
className="mt-3 text-sm text-muted-foreground"
aria-live="polite"
>
{githubSyncedAt ? (
<>
Last synced{' '}
<time
dateTime={githubSyncedAt}
title={formatFullDateTime(githubSyncedAt)}
>
{formatSyncedAgo(githubSyncedAt, now)}
</time>
</>
) : (
'Not yet synced from GitHub'
)}
</p>
</section>

<section className="px-4 py-6 sm:px-6">
<Heading
title="Color"
Expand Down
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@
Route::get('organizations/{organization}/settings/profile', [OrganizationProfileController::class, 'edit'])
->whereUuid('organization')
->name('organization-profile.edit');
Route::put('organizations/{organization}/settings/profile', [OrganizationProfileController::class, 'update'])
->whereUuid('organization')
->middleware('throttle:profile-syncs')
->name('organization-profile.update');
Route::put('organizations/{organization}/settings/lane', [OrganizationLaneController::class, 'update'])
->whereUuid('organization')
->name('organization-lane.update');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
->where('organization.id', $organization->id)
->where('role', 'admin')
->where('organization.lane', '#1a2b3c')
->where('githubSyncedAt', $organization->github_synced_at?->toIso8601String())
->missing('organization.lane_color'));
});

Expand Down
Loading