Skip to content

Commit 721b2ab

Browse files
committed
feat(organization): implement organization settings management, including slug updates for owners and leave organization functionality for members
1 parent b368d02 commit 721b2ab

18 files changed

Lines changed: 738 additions & 27 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Domains\Organization\Contracts\Data;
4+
5+
use App\Models\Organization;
6+
use App\Models\Pivots\OrganizationUserPivot;
7+
use App\Models\User;
8+
use Spatie\LaravelData\Data;
9+
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
10+
11+
#[TypeScript]
12+
class OrganizationWithRoleData extends Data
13+
{
14+
public function __construct(
15+
public OrganizationData $organization,
16+
public string $role,
17+
public bool $isOwner,
18+
public string $pivotUuid,
19+
) {}
20+
21+
public static function fromOrganizationAndPivot(
22+
Organization $organization,
23+
OrganizationUserPivot $pivot,
24+
User $user
25+
): self {
26+
return new self(
27+
organization: OrganizationData::fromModel($organization),
28+
role: $pivot->role,
29+
isOwner: $organization->owner_uuid === $user->uuid,
30+
pivotUuid: $pivot->uuid,
31+
);
32+
}
33+
}

app/Domains/Organization/Http/Controllers/SettingsController.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,30 @@ public function general(Organization $organization): Response
2323
{
2424
$this->authorize('viewSettings', $organization);
2525

26+
$user = auth()->user();
27+
$isOwner = $user !== null && $organization->owner_uuid === $user->uuid;
28+
2629
return Inertia::render('organizations/settings/general', [
2730
'organization' => OrganizationData::from($organization),
31+
'isOwner' => $isOwner,
2832
]);
2933
}
3034

3135
public function update(UpdateOrganizationRequest $request, Organization $organization): RedirectResponse
3236
{
33-
$organization->update([
37+
$user = $request->user();
38+
$isOwner = $user !== null && $organization->owner_uuid === $user->uuid;
39+
40+
$updateData = [
3441
'name' => $request->validated('name'),
35-
]);
42+
];
43+
44+
if ($isOwner && $request->has('slug')) {
45+
$updateData['slug'] = $request->validated('slug');
46+
}
47+
48+
$organization->update($updateData);
49+
$organization->refresh();
3650

3751
return redirect()
3852
->route('organizations.settings.general', $organization)

app/Domains/Organization/Requests/UpdateOrganizationRequest.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Domains\Organization\Requests;
44

55
use Illuminate\Foundation\Http\FormRequest;
6+
use Illuminate\Validation\Rule;
67

78
class UpdateOrganizationRequest extends FormRequest
89
{
@@ -18,12 +19,29 @@ public function authorize(): bool
1819
}
1920

2021
/**
21-
* @return array<string, array<int, string>>
22+
* @return array<string, array<int, string|Rule>>
2223
*/
2324
public function rules(): array
2425
{
25-
return [
26+
/** @var \App\Models\Organization|null $organization */
27+
$organization = $this->route('organization');
28+
$user = $this->user();
29+
$isOwner = $organization !== null && $user !== null && $organization->owner_uuid === $user->uuid;
30+
31+
$rules = [
2632
'name' => ['required', 'string', 'max:255'],
2733
];
34+
35+
if ($isOwner) {
36+
$rules['slug'] = [
37+
'required',
38+
'string',
39+
'max:255',
40+
'regex:/^[a-z0-9]+(?:-[a-z0-9]+)*$/',
41+
Rule::unique('organizations', 'slug')->ignore($organization->uuid, 'uuid'),
42+
];
43+
}
44+
45+
return $rules;
2846
}
2947
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Settings;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Models\Organization;
7+
use App\Models\OrganizationUser;
8+
use Illuminate\Http\RedirectResponse;
9+
use Illuminate\Http\Request;
10+
11+
class LeaveOrganizationController extends Controller
12+
{
13+
public function __invoke(Request $request, Organization $organization): RedirectResponse
14+
{
15+
$user = $request->user();
16+
17+
if ($user === null) {
18+
abort(401);
19+
}
20+
21+
if ($organization->owner_uuid === $user->uuid) {
22+
return redirect()
23+
->route('settings.organizations')
24+
->with('error', 'You cannot leave an organization you own.');
25+
}
26+
27+
$organizationUser = OrganizationUser::where('organization_uuid', $organization->uuid)
28+
->where('user_uuid', $user->uuid)
29+
->first();
30+
31+
if ($organizationUser === null) {
32+
return redirect()
33+
->route('settings.organizations')
34+
->with('error', 'You are not a member of this organization.');
35+
}
36+
37+
$organizationUser->delete();
38+
39+
return redirect()
40+
->route('settings.organizations')
41+
->with('success', 'You have left the organization successfully.');
42+
}
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Settings;
4+
5+
use App\Domains\Organization\Contracts\Data\OrganizationWithRoleData;
6+
use App\Http\Controllers\Controller;
7+
use App\Models\Organization;
8+
use App\Models\Pivots\OrganizationUserPivot;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class OrganizationsController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$user = $request->user();
18+
19+
if ($user === null) {
20+
abort(401);
21+
}
22+
23+
$organizations = $user
24+
->organizations()
25+
->withPivot('role', 'uuid')
26+
->get()
27+
->map(function (Organization $org) use ($user) {
28+
$pivot = $org->pivot;
29+
30+
if (! $pivot instanceof OrganizationUserPivot) {
31+
throw new \RuntimeException('Pivot is not an instance of OrganizationUserPivot');
32+
}
33+
34+
return OrganizationWithRoleData::fromOrganizationAndPivot($org, $pivot, $user);
35+
});
36+
37+
return Inertia::render('settings/organizations', [
38+
'organizations' => $organizations,
39+
]);
40+
}
41+
}

app/Models/User.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,13 @@ public function ownedOrganizations(): HasMany
100100
}
101101

102102
/**
103-
* @return BelongsToMany<Organization, $this>
103+
* @return BelongsToMany<Organization, $this, OrganizationUserPivot, 'pivot'>
104104
*/
105105
public function organizations(): BelongsToMany
106106
{
107107
return $this->belongsToMany(Organization::class, 'organization_users', 'user_uuid', 'organization_uuid', 'uuid', 'uuid')
108-
->withPivot('role')
108+
->using(OrganizationUserPivot::class)
109+
->withPivot('role', 'uuid')
109110
->withTimestamps();
110111
}
111112

app/Policies/OrganizationPolicy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ public function manageMembers(User $user, Organization $organization): bool
3838
{
3939
return $this->viewSettings($user, $organization);
4040
}
41+
42+
public function updateSlug(User $user, Organization $organization): bool
43+
{
44+
return $organization->owner_uuid === $user->uuid;
45+
}
4146
}

resources/js/components/app-logo.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import AppLogoIcon from './app-logo-icon';
22

33
export default function AppLogo() {
44
return (
5-
<>
5+
<div className="flex w-full items-center justify-center gap-2">
66
<div className="flex aspect-square size-6 items-center justify-center rounded-md text-sidebar-primary-foreground">
77
<AppLogoIcon className="size-6 fill-current text-white dark:text-black" />
88
</div>
9-
<div className="ml-1 grid flex-1 text-left text-sm group-data-[collapsible=icon]:hidden">
9+
<div className="grid text-center text-lg group-data-[collapsible=icon]:hidden">
1010
<span className="mb-0.5 truncate leading-tight font-semibold">
1111
Pricore
1212
</span>
1313
</div>
14-
</>
14+
</div>
1515
);
1616
}

resources/js/components/app-sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function AppSidebar() {
8484
<SidebarMenuButton
8585
size="lg"
8686
asChild
87-
className="group-data-[collapsible=icon]:justify-center"
87+
className="justify-center"
8888
>
8989
<Link href={dashboard()} prefetch>
9090
<AppLogo />

resources/js/components/info-box.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default function InfoBox({
1010
children,
1111
}: InfoBoxProps) {
1212
return (
13-
<div className="rounded-md border border-b-2 border-neutral-200 bg-neutral-50 p-4 dark:border-neutral-800 dark:bg-neutral-950">
13+
<div className="rounded-md border border-b-2 border-neutral-200 bg-neutral-50 p-5 dark:border-neutral-800 dark:bg-neutral-950">
1414
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
1515
{title}
1616
</p>
@@ -21,4 +21,3 @@ export default function InfoBox({
2121
</div>
2222
);
2323
}
24-

0 commit comments

Comments
 (0)