Renamed jetstream team to organization

This commit is contained in:
Constantin Graf
2024-01-21 19:52:19 +01:00
parent 6377df1f44
commit 0a8d958ccc
63 changed files with 461 additions and 329 deletions

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@@ -46,9 +46,9 @@ class CreateNewUser implements CreatesNewUsers
*/ */
protected function createTeam(User $user): void protected function createTeam(User $user): void
{ {
$user->ownedTeams()->save(Team::forceCreate([ $user->ownedTeams()->save(Organization::forceCreate([
'user_id' => $user->id, 'user_id' => $user->id,
'name' => explode(' ', $user->name, 2)[0]."'s Team", 'name' => explode(' ', $user->name, 2)[0]."'s Organization",
'personal_team' => true, 'personal_team' => true,
])); ]));
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Closure; use Closure;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@@ -15,32 +15,32 @@ use Laravel\Jetstream\Events\TeamMemberAdded;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Rules\Role; use Laravel\Jetstream\Rules\Role;
class AddTeamMember implements AddsTeamMembers class AddOrganizationMember implements AddsTeamMembers
{ {
/** /**
* Add a new team member to the given team. * Add a new team member to the given team.
*/ */
public function add(User $user, Team $team, string $email, ?string $role = null): void public function add(User $user, Organization $organization, string $email, ?string $role = null): void
{ {
Gate::forUser($user)->authorize('addTeamMember', $team); Gate::forUser($user)->authorize('addTeamMember', $organization);
$this->validate($team, $email, $role); $this->validate($organization, $email, $role);
$newTeamMember = Jetstream::findUserByEmailOrFail($email); $newTeamMember = Jetstream::findUserByEmailOrFail($email);
AddingTeamMember::dispatch($team, $newTeamMember); AddingTeamMember::dispatch($organization, $newTeamMember);
$team->users()->attach( $organization->users()->attach(
$newTeamMember, ['role' => $role] $newTeamMember, ['role' => $role]
); );
TeamMemberAdded::dispatch($team, $newTeamMember); TeamMemberAdded::dispatch($organization, $newTeamMember);
} }
/** /**
* Validate the add member operation. * Validate the add member operation.
*/ */
protected function validate(Team $team, string $email, ?string $role): void protected function validate(Organization $organization, string $email, ?string $role): void
{ {
Validator::make([ Validator::make([
'email' => $email, 'email' => $email,
@@ -48,7 +48,7 @@ class AddTeamMember implements AddsTeamMembers
], $this->rules(), [ ], $this->rules(), [
'email.exists' => __('We were unable to find a registered user with this email address.'), 'email.exists' => __('We were unable to find a registered user with this email address.'),
])->after( ])->after(
$this->ensureUserIsNotAlreadyOnTeam($team, $email) $this->ensureUserIsNotAlreadyOnTeam($organization, $email)
)->validateWithBag('addTeamMember'); )->validateWithBag('addTeamMember');
} }
@@ -70,7 +70,7 @@ class AddTeamMember implements AddsTeamMembers
/** /**
* Ensure that the user is not already on the team. * Ensure that the user is not already on the team.
*/ */
protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure protected function ensureUserIsNotAlreadyOnTeam(Organization $team, string $email): Closure
{ {
return function ($validator) use ($team, $email) { return function ($validator) use ($team, $email) {
$validator->errors()->addIf( $validator->errors()->addIf(

View File

@@ -4,22 +4,27 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\CreatesTeams; use Laravel\Jetstream\Contracts\CreatesTeams;
use Laravel\Jetstream\Events\AddingTeam; use Laravel\Jetstream\Events\AddingTeam;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
class CreateTeam implements CreatesTeams class CreateOrganization implements CreatesTeams
{ {
/** /**
* Validate and create a new team for the given user. * Validate and create a new team for the given user.
* *
* @param array<string, string> $input * @param array<string, string> $input
*
* @throws AuthorizationException
* @throws ValidationException
*/ */
public function create(User $user, array $input): Team public function create(User $user, array $input): Organization
{ {
Gate::forUser($user)->authorize('create', Jetstream::newTeamModel()); Gate::forUser($user)->authorize('create', Jetstream::newTeamModel());
@@ -29,11 +34,14 @@ class CreateTeam implements CreatesTeams
AddingTeam::dispatch($user); AddingTeam::dispatch($user);
$user->switchTeam($team = $user->ownedTeams()->create([ /** @var Organization $organization */
$organization = $user->ownedTeams()->create([
'name' => $input['name'], 'name' => $input['name'],
'personal_team' => false, 'personal_team' => false,
])); ]);
return $team; $user->switchTeam($organization);
return $organization;
} }
} }

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use Laravel\Jetstream\Contracts\DeletesTeams; use Laravel\Jetstream\Contracts\DeletesTeams;
class DeleteTeam implements DeletesTeams class DeleteOrganization implements DeletesTeams
{ {
/** /**
* Delete the given team. * Delete the given team.
*/ */
public function delete(Team $team): void public function delete(Organization $team): void
{ {
$team->purge(); $team->purge();
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Laravel\Jetstream\Contracts\DeletesTeams; use Laravel\Jetstream\Contracts\DeletesTeams;
@@ -47,7 +47,7 @@ class DeleteUser implements DeletesUsers
{ {
$user->teams()->detach(); $user->teams()->detach();
$user->ownedTeams->each(function (Team $team) { $user->ownedTeams->each(function (Organization $team) {
$this->deletesTeams->delete($team); $this->deletesTeams->delete($team);
}); });
} }

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Events\InvitingTeamMember;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Mail\TeamInvitation;
use Laravel\Jetstream\Rules\Role;
class InviteOrganizationMember implements InvitesTeamMembers
{
/**
* Invite a new team member to the given team.
*/
public function invite(User $user, Organization $organization, string $email, ?string $role = null): void
{
Gate::forUser($user)->authorize('addTeamMember', $organization);
$this->validate($organization, $email, $role);
InvitingTeamMember::dispatch($organization, $email, $role);
$invitation = $organization->teamInvitations()->create([
'email' => $email,
'role' => $role,
]);
Mail::to($email)->send(new TeamInvitation($invitation));
}
/**
* Validate the invite member operation.
*/
protected function validate(Organization $organization, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules($organization), [
'email.unique' => __('This user has already been invited to the team.'),
])->after(
$this->ensureUserIsNotAlreadyOnTeam($organization, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for inviting a team member.
*
* @return array<string, ValidationRule|array|string>
*/
protected function rules(Organization $organization): array
{
return array_filter([
'email' => [
'required',
'email',
new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder) use ($organization) {
/** @var Builder<OrganizationInvitation> $builder */
return $builder->whereBelongsTo($organization, 'organization');
}),
],
'role' => Jetstream::hasRoles()
? ['required', 'string', new Role]
: null,
]);
}
/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Organization $organization, string $email): Closure
{
return function ($validator) use ($organization, $email) {
$validator->errors()->addIf(
$organization->hasUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}

View File

@@ -1,90 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use App\Models\Team;
use App\Models\User;
use Closure;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Jetstream\Contracts\InvitesTeamMembers;
use Laravel\Jetstream\Events\InvitingTeamMember;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\Mail\TeamInvitation;
use Laravel\Jetstream\Rules\Role;
class InviteTeamMember implements InvitesTeamMembers
{
/**
* Invite a new team member to the given team.
*/
public function invite(User $user, Team $team, string $email, ?string $role = null): void
{
Gate::forUser($user)->authorize('addTeamMember', $team);
$this->validate($team, $email, $role);
InvitingTeamMember::dispatch($team, $email, $role);
$invitation = $team->teamInvitations()->create([
'email' => $email,
'role' => $role,
]);
Mail::to($email)->send(new TeamInvitation($invitation));
}
/**
* Validate the invite member operation.
*/
protected function validate(Team $team, string $email, ?string $role): void
{
Validator::make([
'email' => $email,
'role' => $role,
], $this->rules($team), [
'email.unique' => __('This user has already been invited to the team.'),
])->after(
$this->ensureUserIsNotAlreadyOnTeam($team, $email)
)->validateWithBag('addTeamMember');
}
/**
* Get the validation rules for inviting a team member.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function rules(Team $team): array
{
return array_filter([
'email' => [
'required', 'email',
Rule::unique('team_invitations')->where(function (Builder $query) use ($team) {
$query->where('team_id', $team->id);
}),
],
'role' => Jetstream::hasRoles()
? ['required', 'string', new Role]
: null,
]);
}
/**
* Ensure that the user is not already on the team.
*/
protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure
{
return function ($validator) use ($team, $email) {
$validator->errors()->addIf(
$team->hasUserWithEmail($email),
'email',
__('This user already belongs to the team.')
);
};
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
@@ -12,28 +12,28 @@ use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\RemovesTeamMembers; use Laravel\Jetstream\Contracts\RemovesTeamMembers;
use Laravel\Jetstream\Events\TeamMemberRemoved; use Laravel\Jetstream\Events\TeamMemberRemoved;
class RemoveTeamMember implements RemovesTeamMembers class RemoveOrganizationMember implements RemovesTeamMembers
{ {
/** /**
* Remove the team member from the given team. * Remove the team member from the given team.
*/ */
public function remove(User $user, Team $team, User $teamMember): void public function remove(User $user, Organization $organization, User $teamMember): void
{ {
$this->authorize($user, $team, $teamMember); $this->authorize($user, $organization, $teamMember);
$this->ensureUserDoesNotOwnTeam($teamMember, $team); $this->ensureUserDoesNotOwnTeam($teamMember, $organization);
$team->removeUser($teamMember); $organization->removeUser($teamMember);
TeamMemberRemoved::dispatch($team, $teamMember); TeamMemberRemoved::dispatch($organization, $teamMember);
} }
/** /**
* Authorize that the user can remove the team member. * Authorize that the user can remove the team member.
*/ */
protected function authorize(User $user, Team $team, User $teamMember): void protected function authorize(User $user, Organization $organization, User $teamMember): void
{ {
if (! Gate::forUser($user)->check('removeTeamMember', $team) && if (! Gate::forUser($user)->check('removeTeamMember', $organization) &&
$user->id !== $teamMember->id) { $user->id !== $teamMember->id) {
throw new AuthorizationException; throw new AuthorizationException;
} }
@@ -42,9 +42,9 @@ class RemoveTeamMember implements RemovesTeamMembers
/** /**
* Ensure that the currently authenticated user does not own the team. * Ensure that the currently authenticated user does not own the team.
*/ */
protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void protected function ensureUserDoesNotOwnTeam(User $teamMember, Organization $organization): void
{ {
if ($teamMember->id === $team->owner->id) { if ($teamMember->id === $organization->owner->id) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'team' => [__('You may not leave a team that you created.')], 'team' => [__('You may not leave a team that you created.')],
])->errorBag('removeTeamMember'); ])->errorBag('removeTeamMember');

View File

@@ -4,28 +4,33 @@ declare(strict_types=1);
namespace App\Actions\Jetstream; namespace App\Actions\Jetstream;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\UpdatesTeamNames; use Laravel\Jetstream\Contracts\UpdatesTeamNames;
class UpdateTeamName implements UpdatesTeamNames class UpdateOrganization implements UpdatesTeamNames
{ {
/** /**
* Validate and update the given team's name. * Validate and update the given team's name.
* *
* @param array<string, string> $input * @param array<string, string> $input
*
* @throws AuthorizationException
* @throws ValidationException
*/ */
public function update(User $user, Team $team, array $input): void public function update(User $user, Organization $organization, array $input): void
{ {
Gate::forUser($user)->authorize('update', $team); Gate::forUser($user)->authorize('update', $organization);
Validator::make($input, [ Validator::make($input, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
])->validateWithBag('updateTeamName'); ])->validateWithBag('updateTeamName');
$team->forceFill([ $organization->forceFill([
'name' => $input['name'], 'name' => $input['name'],
])->save(); ])->save();
} }

View File

@@ -14,7 +14,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $id * @property string $id
* @property string $name * @property string $name
* @property string $organization_id * @property string $organization_id
* @property-read Team $organization * @property string $created_at
* @property string $updated_at
* @property-read Organization $organization
* *
* @method static ClientFactory factory() * @method static ClientFactory factory()
*/ */
@@ -33,10 +35,10 @@ class Client extends Model
]; ];
/** /**
* @return BelongsTo<Team, Client> * @return BelongsTo<Organization, Client>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Team::class, 'organization_id'); return $this->belongsTo(Organization::class, 'organization_id');
} }
} }

View File

@@ -7,14 +7,24 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Laravel\Jetstream\Membership as JetstreamMembership; use Laravel\Jetstream\Membership as JetstreamMembership;
/**
* @property string $id
* @property string $role
* @property string $organization_id
* @property string $user_id
* @property string $created_at
* @property string $updated_at
* @property-read Organization $organization
* @property-read User $user
*/
class Membership extends JetstreamMembership class Membership extends JetstreamMembership
{ {
use HasUuids; use HasUuids;
/** /**
* Indicates if the IDs are auto-incrementing. * The table associated with the pivot model.
* *
* @var bool * @var string
*/ */
public $incrementing = true; protected $table = 'organization_user';
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Database\Factories\TeamFactory; use Database\Factories\OrganizationFactory;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -17,10 +17,10 @@ use Laravel\Jetstream\Team as JetstreamTeam;
* @property string $id * @property string $id
* @property User $owner * @property User $owner
* *
* @method HasMany<TeamInvitation> teamInvitations() * @method HasMany<OrganizationInvitation> teamInvitations()
* @method static TeamFactory factory() * @method static OrganizationFactory factory()
*/ */
class Team extends JetstreamTeam class Organization extends JetstreamTeam
{ {
use HasFactory; use HasFactory;
use HasUuids; use HasUuids;

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\TeamInvitation as JetstreamTeamInvitation;
/**
* @property string $id
* @property string $email
*/
class OrganizationInvitation extends JetstreamTeamInvitation
{
use HasUuids;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'organization_invitations';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
'role',
];
/**
* Get the organization that the invitation belongs to.
*/
public function organization(): BelongsTo
{
return $this->belongsTo(Jetstream::teamModel(), 'organization_id');
}
/**
* Get the organization that the invitation belongs to.
*/
public function team(): BelongsTo
{
return $this->belongsTo(Jetstream::teamModel(), 'organization_id');
}
}

View File

@@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property string $name * @property string $name
* @property string $organization_id * @property string $organization_id
* @property string $client_id * @property string $client_id
* @property-read Team $organization * @property-read Organization $organization
* @property-read Client|null $client * @property-read Client|null $client
* @property-read Collection<Task> $tasks * @property-read Collection<Task> $tasks
* *
@@ -38,11 +38,11 @@ class Project extends Model
]; ];
/** /**
* @return BelongsTo<Team, Project> * @return BelongsTo<Organization, Project>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Team::class, 'organization_id'); return $this->belongsTo(Organization::class, 'organization_id');
} }
/** /**

View File

@@ -14,7 +14,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $id * @property string $id
* @property string $name * @property string $name
* @property string $organization_id * @property string $organization_id
* @property-read Team $organization * @property-read Organization $organization
* *
* @method static TagFactory factory() * @method static TagFactory factory()
*/ */
@@ -33,10 +33,10 @@ class Tag extends Model
]; ];
/** /**
* @return BelongsTo<Team, Tag> * @return BelongsTo<Organization, Tag>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Team::class, 'organization_id'); return $this->belongsTo(Organization::class, 'organization_id');
} }
} }

View File

@@ -16,7 +16,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property string $project_id * @property string $project_id
* @property string $organization_id * @property string $organization_id
* @property-read Project $project * @property-read Project $project
* @property-read Team $organization * @property-read Organization $organization
* *
* @method static TaskFactory factory() * @method static TaskFactory factory()
*/ */
@@ -43,10 +43,10 @@ class Task extends Model
} }
/** /**
* @return BelongsTo<Team, Task> * @return BelongsTo<Organization, Task>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Team::class, 'organization_id'); return $this->belongsTo(Organization::class, 'organization_id');
} }
} }

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Jetstream\Jetstream;
use Laravel\Jetstream\TeamInvitation as JetstreamTeamInvitation;
class TeamInvitation extends JetstreamTeamInvitation
{
use HasUuids;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
'role',
];
/**
* Get the team that the invitation belongs to.
*/
public function team(): BelongsTo
{
return $this->belongsTo(Jetstream::teamModel());
}
}

View File

@@ -19,7 +19,7 @@ use Illuminate\Support\Carbon;
* @property bool $billable * @property bool $billable
* @property array $tags * @property array $tags
* @property-read User $user * @property-read User $user
* @property-read Team $organization * @property-read Organization $organization
* @property-read Project|null $project * @property-read Project|null $project
* @property-read Task|null $task * @property-read Task|null $task
* *
@@ -52,11 +52,11 @@ class TimeEntry extends Model
} }
/** /**
* @return BelongsTo<Team, TimeEntry> * @return BelongsTo<Organization, TimeEntry>
*/ */
public function organization(): BelongsTo public function organization(): BelongsTo
{ {
return $this->belongsTo(Team::class, 'organization_id'); return $this->belongsTo(Organization::class, 'organization_id');
} }
/** /**

View File

@@ -8,6 +8,7 @@ use Database\Factories\UserFactory;
use Filament\Panel; use Filament\Panel;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@@ -20,7 +21,7 @@ use Laravel\Passport\HasApiTokens;
* @property string $id * @property string $id
* @property string $name * @property string $name
* *
* @method HasMany<Team> ownedTeams() * @method HasMany<Organization> ownedTeams()
* @method static UserFactory factory() * @method static UserFactory factory()
*/ */
class User extends Authenticatable class User extends Authenticatable
@@ -79,4 +80,17 @@ class User extends Authenticatable
// TODO: Implement canAccessPanel() method. // TODO: Implement canAccessPanel() method.
return false; return false;
} }
/**
* @return BelongsToMany<Organization>
*/
public function organizations(): BelongsToMany
{
return $this->belongsToMany(Organization::class, Membership::class)
->withPivot([
'role',
])
->withTimestamps()
->as('membership');
}
} }

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Policies; namespace App\Policies;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Auth\Access\HandlesAuthorization;
class TeamPolicy class OrganizationPolicy
{ {
use HandlesAuthorization; use HandlesAuthorization;
@@ -23,9 +23,9 @@ class TeamPolicy
/** /**
* Determine whether the user can view the model. * Determine whether the user can view the model.
*/ */
public function view(User $user, Team $team): bool public function view(User $user, Organization $organization): bool
{ {
return $user->belongsToTeam($team); return $user->belongsToTeam($organization);
} }
/** /**
@@ -39,40 +39,40 @@ class TeamPolicy
/** /**
* Determine whether the user can update the model. * Determine whether the user can update the model.
*/ */
public function update(User $user, Team $team): bool public function update(User $user, Organization $organization): bool
{ {
return $user->ownsTeam($team); return $user->ownsTeam($organization);
} }
/** /**
* Determine whether the user can add team members. * Determine whether the user can add team members.
*/ */
public function addTeamMember(User $user, Team $team): bool public function addTeamMember(User $user, Organization $organization): bool
{ {
return $user->ownsTeam($team); return $user->ownsTeam($organization);
} }
/** /**
* Determine whether the user can update team member permissions. * Determine whether the user can update team member permissions.
*/ */
public function updateTeamMember(User $user, Team $team): bool public function updateTeamMember(User $user, Organization $organization): bool
{ {
return $user->ownsTeam($team); return $user->ownsTeam($organization);
} }
/** /**
* Determine whether the user can remove team members. * Determine whether the user can remove team members.
*/ */
public function removeTeamMember(User $user, Team $team): bool public function removeTeamMember(User $user, Organization $organization): bool
{ {
return $user->ownsTeam($team); return $user->ownsTeam($organization);
} }
/** /**
* Determine whether the user can delete the model. * Determine whether the user can delete the model.
*/ */
public function delete(User $user, Team $team): bool public function delete(User $user, Organization $organization): bool
{ {
return $user->ownsTeam($team); return $user->ownsTeam($organization);
} }
} }

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use App\Models\Membership; use App\Models\Membership;
use App\Models\Team; use App\Models\Organization;
use App\Models\TeamInvitation; use App\Models\OrganizationInvitation;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
@@ -31,8 +31,8 @@ class AppServiceProvider extends ServiceProvider
Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction()); Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
Relation::enforceMorphMap([ Relation::enforceMorphMap([
'membership' => Membership::class, 'membership' => Membership::class,
'team' => Team::class, 'team' => Organization::class,
'team_invitation' => TeamInvitation::class, 'team_invitation' => OrganizationInvitation::class,
'user' => User::class, 'user' => User::class,
]); ]);
} }

View File

@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
// use Illuminate\Support\Facades\Gate; use App\Models\Organization;
use App\Policies\OrganizationPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
use Laravel\Passport\Passport; use Laravel\Passport\Passport;
@@ -17,7 +18,7 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string> * @var array<class-string, class-string>
*/ */
protected $policies = [ protected $policies = [
// Organization::class => OrganizationPolicy::class,
]; ];
/** /**

View File

@@ -4,13 +4,15 @@ declare(strict_types=1);
namespace App\Providers; namespace App\Providers;
use App\Actions\Jetstream\AddTeamMember; use App\Actions\Jetstream\AddOrganizationMember;
use App\Actions\Jetstream\CreateTeam; use App\Actions\Jetstream\CreateOrganization;
use App\Actions\Jetstream\DeleteTeam; use App\Actions\Jetstream\DeleteOrganization;
use App\Actions\Jetstream\DeleteUser; use App\Actions\Jetstream\DeleteUser;
use App\Actions\Jetstream\InviteTeamMember; use App\Actions\Jetstream\InviteOrganizationMember;
use App\Actions\Jetstream\RemoveTeamMember; use App\Actions\Jetstream\RemoveOrganizationMember;
use App\Actions\Jetstream\UpdateTeamName; use App\Actions\Jetstream\UpdateOrganization;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Jetstream\Jetstream; use Laravel\Jetstream\Jetstream;
@@ -31,13 +33,15 @@ class JetstreamServiceProvider extends ServiceProvider
{ {
$this->configurePermissions(); $this->configurePermissions();
Jetstream::createTeamsUsing(CreateTeam::class); Jetstream::createTeamsUsing(CreateOrganization::class);
Jetstream::updateTeamNamesUsing(UpdateTeamName::class); Jetstream::updateTeamNamesUsing(UpdateOrganization::class);
Jetstream::addTeamMembersUsing(AddTeamMember::class); Jetstream::addTeamMembersUsing(AddOrganizationMember::class);
Jetstream::inviteTeamMembersUsing(InviteTeamMember::class); Jetstream::inviteTeamMembersUsing(InviteOrganizationMember::class);
Jetstream::removeTeamMembersUsing(RemoveTeamMember::class); Jetstream::removeTeamMembersUsing(RemoveOrganizationMember::class);
Jetstream::deleteTeamsUsing(DeleteTeam::class); Jetstream::deleteTeamsUsing(DeleteOrganization::class);
Jetstream::deleteUsersUsing(DeleteUser::class); Jetstream::deleteUsersUsing(DeleteUser::class);
Jetstream::useTeamModel(Organization::class);
Jetstream::useTeamInvitationModel(OrganizationInvitation::class);
} }
/** /**

View File

@@ -9,6 +9,7 @@
"filament/filament": "^3.2", "filament/filament": "^3.2",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"inertiajs/inertia-laravel": "^0.6.8", "inertiajs/inertia-laravel": "^0.6.8",
"korridor/laravel-model-validation-rules": "^3.0",
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/jetstream": "^4.2", "laravel/jetstream": "^4.2",
"laravel/passport": "*", "laravel/passport": "*",

66
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "34dbd2208afb93ce559614a5c014397e", "content-hash": "68ea9179372fd4db583f50784a1ca6f7",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -2509,6 +2509,70 @@
}, },
"time": "2023-12-07T10:44:41+00:00" "time": "2023-12-07T10:44:41+00:00"
}, },
{
"name": "korridor/laravel-model-validation-rules",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/korridor/laravel-model-validation-rules.git",
"reference": "23537e5bd296a042bbe0c3a1c6d556c89dfbad42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/korridor/laravel-model-validation-rules/zipball/23537e5bd296a042bbe0c3a1c6d556c89dfbad42",
"reference": "23537e5bd296a042bbe0c3a1c6d556c89dfbad42",
"shasum": ""
},
"require": {
"illuminate/database": "^10",
"illuminate/support": "^10",
"php": ">=8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.6",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^10",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Korridor\\LaravelModelValidationRules\\ModelValidationServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Korridor\\LaravelModelValidationRules\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "korridor",
"email": "26689068+korridor@users.noreply.github.com"
}
],
"description": "A laravel validation rule that uses eloquent to validate if a model exists",
"homepage": "https://github.com/korridor/laravel-model-validation-rules",
"keywords": [
"eloquent",
"exist",
"laravel",
"model",
"rule",
"validation"
],
"support": {
"issues": "https://github.com/korridor/laravel-model-validation-rules/issues",
"source": "https://github.com/korridor/laravel-model-validation-rules/tree/3.0.0"
},
"time": "2023-02-16T11:15:08+00:00"
},
{ {
"name": "laravel/fortify", "name": "laravel/fortify",
"version": "v1.20.0", "version": "v1.20.0",

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Client; use App\Models\Client;
use App\Models\Team; use App\Models\Organization;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@@ -22,11 +22,11 @@ class ClientFactory extends Factory
{ {
return [ return [
'name' => $this->faker->company(), 'name' => $this->faker->company(),
'organization_id' => Team::factory(), 'organization_id' => Organization::factory(),
]; ];
} }
public function forOrganization(Team $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {
return [ return [

View File

@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Team> * @extends Factory<Organization>
*/ */
class TeamFactory extends Factory class OrganizationFactory extends Factory
{ {
/** /**
* Define the model's default state. * Define the model's default state.

View File

@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Client; use App\Models\Client;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Team;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@@ -24,12 +24,12 @@ class ProjectFactory extends Factory
return [ return [
'name' => $this->faker->company(), 'name' => $this->faker->company(),
'color' => $this->faker->hexColor(), 'color' => $this->faker->hexColor(),
'organization_id' => Team::factory(), 'organization_id' => Organization::factory(),
'client_id' => null, 'client_id' => null,
]; ];
} }
public function forOrganization(Team $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {
return [ return [

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Organization;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Team;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@@ -22,11 +22,11 @@ class TagFactory extends Factory
{ {
return [ return [
'name' => $this->faker->name, 'name' => $this->faker->name,
'organization_id' => Team::factory(), 'organization_id' => Organization::factory(),
]; ];
} }
public function forOrganization(Team $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {
return [ return [

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
@@ -24,7 +24,7 @@ class TaskFactory extends Factory
return [ return [
'name' => $this->faker->word(), 'name' => $this->faker->word(),
'project_id' => Project::factory(), 'project_id' => Project::factory(),
'organization_id' => Team::factory(), 'organization_id' => Organization::factory(),
]; ];
} }
@@ -37,7 +37,7 @@ class TaskFactory extends Factory
}); });
} }
public function forOrganization(Team $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {
return [ return [

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
@@ -32,7 +32,7 @@ class TimeEntryFactory extends Factory
'billable' => $this->faker->boolean(), 'billable' => $this->faker->boolean(),
'tags' => [], 'tags' => [],
'user_id' => User::factory(), 'user_id' => User::factory(),
'organization_id' => Team::factory(), 'organization_id' => Organization::factory(),
]; ];
} }
@@ -45,7 +45,7 @@ class TimeEntryFactory extends Factory
}); });
} }
public function forOrganization(Team $organization): self public function forOrganization(Organization $organization): self
{ {
return $this->state(function (array $attributes) use ($organization) { return $this->state(function (array $attributes) use ($organization) {
return [ return [

View File

@@ -4,14 +4,13 @@ declare(strict_types=1);
namespace Database\Factories; namespace Database\Factories;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Jetstream\Features;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> * @extends Factory<User>
*/ */
class UserFactory extends Factory class UserFactory extends Factory
{ {
@@ -50,16 +49,12 @@ class UserFactory extends Factory
/** /**
* Indicate that the user should have a personal team. * Indicate that the user should have a personal team.
*/ */
public function withPersonalTeam(?callable $callback = null): static public function withPersonalOrganization(?callable $callback = null): static
{ {
if (! Features::hasTeamFeatures()) {
return $this->state([]);
}
return $this->has( return $this->has(
Team::factory() Organization::factory()
->state(fn (array $attributes, User $user) => [ ->state(fn (array $attributes, User $user) => [
'name' => $user->name.'\'s Team', 'name' => $user->name.'\'s Organization',
'user_id' => $user->id, 'user_id' => $user->id,
'personal_team' => true, 'personal_team' => true,
]) ])

View File

@@ -13,7 +13,7 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('teams', function (Blueprint $table) { Schema::create('organizations', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('user_id')->index(); $table->foreignUuid('user_id')->index();
$table->string('name'); $table->string('name');

View File

@@ -13,14 +13,14 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('team_user', function (Blueprint $table) { Schema::create('organization_user', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('team_id'); $table->foreignUuid('organization_id');
$table->foreignUuid('user_id'); $table->foreignUuid('user_id');
$table->string('role')->nullable(); $table->string('role')->nullable();
$table->timestamps(); $table->timestamps();
$table->unique(['team_id', 'user_id']); $table->unique(['organization_id', 'user_id']);
}); });
} }

View File

@@ -13,16 +13,16 @@ return new class extends Migration
*/ */
public function up(): void public function up(): void
{ {
Schema::create('team_invitations', function (Blueprint $table) { Schema::create('organization_invitations', function (Blueprint $table) {
$table->uuid('id')->primary(); $table->uuid('id')->primary();
$table->foreignUuid('team_id') $table->foreignUuid('organization_id')
->constrained() ->constrained()
->cascadeOnDelete(); ->cascadeOnDelete();
$table->string('email'); $table->string('email');
$table->string('role')->nullable(); $table->string('role')->nullable();
$table->timestamps(); $table->timestamps();
$table->unique(['team_id', 'email']); $table->unique(['organization_id', 'email']);
}); });
} }

View File

@@ -19,7 +19,7 @@ return new class extends Migration
$table->uuid('organization_id'); $table->uuid('organization_id');
$table->foreign('organization_id') $table->foreign('organization_id')
->references('id') ->references('id')
->on('teams') ->on('organizations')
->cascadeOnUpdate() ->cascadeOnUpdate()
->restrictOnDelete(); ->restrictOnDelete();
$table->timestamps(); $table->timestamps();

View File

@@ -26,7 +26,7 @@ return new class extends Migration
$table->uuid('organization_id'); $table->uuid('organization_id');
$table->foreign('organization_id') $table->foreign('organization_id')
->references('id') ->references('id')
->on('teams') ->on('organizations')
->cascadeOnUpdate() ->cascadeOnUpdate()
->restrictOnDelete(); ->restrictOnDelete();
$table->timestamps(); $table->timestamps();

View File

@@ -25,7 +25,7 @@ return new class extends Migration
$table->uuid('organization_id'); $table->uuid('organization_id');
$table->foreign('organization_id') $table->foreign('organization_id')
->references('id') ->references('id')
->on('teams') ->on('organizations')
->cascadeOnUpdate() ->cascadeOnUpdate()
->restrictOnDelete(); ->restrictOnDelete();
$table->timestamps(); $table->timestamps();

View File

@@ -19,7 +19,7 @@ return new class extends Migration
$table->uuid('organization_id'); $table->uuid('organization_id');
$table->foreign('organization_id') $table->foreign('organization_id')
->references('id') ->references('id')
->on('teams') ->on('organizations')
->cascadeOnUpdate() ->cascadeOnUpdate()
->restrictOnDelete(); ->restrictOnDelete();
$table->timestamps(); $table->timestamps();

View File

@@ -28,7 +28,7 @@ return new class extends Migration
$table->uuid('organization_id'); $table->uuid('organization_id');
$table->foreign('organization_id') $table->foreign('organization_id')
->references('id') ->references('id')
->on('teams') ->on('organizations')
->cascadeOnUpdate() ->cascadeOnUpdate()
->restrictOnDelete(); ->restrictOnDelete();
$table->uuid('project_id')->nullable(); $table->uuid('project_id')->nullable();

View File

@@ -6,9 +6,9 @@ namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents; // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\Client; use App\Models\Client;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@@ -22,14 +22,23 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$this->deleteAll(); $this->deleteAll();
$organization = Team::factory()->create([ $organization = Organization::factory()->create([
'name' => 'ACME Corp', 'name' => 'ACME Corp',
]); ]);
$user1 = User::factory()->withPersonalTeam()->create([ $user1 = User::factory()->withPersonalOrganization()->create([
'name' => 'Test User', 'name' => 'Test User',
'email' => 'test@example.com', 'email' => 'test@example.com',
]); ]);
$user1->teams()->attach($organization); $userAcmeAdmin = User::factory()->create([
'name' => 'ACME Admin',
'email' => 'admin@acme.test',
]);
$user1->organizations()->attach($organization, [
'role' => 'editor',
]);
$userAcmeAdmin->organizations()->attach($organization, [
'role' => 'admin',
]);
$client = Client::factory()->create([ $client = Client::factory()->create([
'name' => 'Big Company', 'name' => 'Big Company',
]); ]);
@@ -50,6 +59,6 @@ class DatabaseSeeder extends Seeder
DB::table((new Project())->getTable())->delete(); DB::table((new Project())->getTable())->delete();
DB::table((new Client())->getTable())->delete(); DB::table((new Client())->getTable())->delete();
DB::table((new User())->getTable())->delete(); DB::table((new User())->getTable())->delete();
DB::table((new Team())->getTable())->delete(); DB::table((new Organization())->getTable())->delete();
} }
} }

View File

@@ -72,12 +72,12 @@ const logout = () => {
<template #content> <template #content>
<div class="w-60"> <div class="w-60">
<!-- Team Management --> <!-- Organization Management -->
<div class="block px-4 py-2 text-xs text-gray-400"> <div class="block px-4 py-2 text-xs text-gray-400">
Manage Team Manage Team
</div> </div>
<!-- Team Settings --> <!-- Organization Settings -->
<DropdownLink :href="route('teams.show', $page.props.auth.user.current_team)"> <DropdownLink :href="route('teams.show', $page.props.auth.user.current_team)">
Team Settings Team Settings
</DropdownLink> </DropdownLink>
@@ -86,7 +86,7 @@ const logout = () => {
Create New Team Create New Team
</DropdownLink> </DropdownLink>
<!-- Team Switcher --> <!-- Organization Switcher -->
<template v-if="$page.props.auth.user.all_teams.length > 1"> <template v-if="$page.props.auth.user.all_teams.length > 1">
<div class="border-t border-gray-200 dark:border-gray-600" /> <div class="border-t border-gray-200 dark:border-gray-600" />
@@ -229,7 +229,7 @@ const logout = () => {
</ResponsiveNavLink> </ResponsiveNavLink>
</form> </form>
<!-- Team Management --> <!-- Organization Management -->
<template v-if="$page.props.jetstream.hasTeamFeatures"> <template v-if="$page.props.jetstream.hasTeamFeatures">
<div class="border-t border-gray-200 dark:border-gray-600" /> <div class="border-t border-gray-200 dark:border-gray-600" />
@@ -237,7 +237,7 @@ const logout = () => {
Manage Team Manage Team
</div> </div>
<!-- Team Settings --> <!-- Organization Settings -->
<ResponsiveNavLink :href="route('teams.show', $page.props.auth.user.current_team)" :active="route().current('teams.show')"> <ResponsiveNavLink :href="route('teams.show', $page.props.auth.user.current_team)" :active="route().current('teams.show')">
Team Settings Team Settings
</ResponsiveNavLink> </ResponsiveNavLink>
@@ -246,7 +246,7 @@ const logout = () => {
Create New Team Create New Team
</ResponsiveNavLink> </ResponsiveNavLink>
<!-- Team Switcher --> <!-- Organization Switcher -->
<template v-if="$page.props.auth.user.all_teams.length > 1"> <template v-if="$page.props.auth.user.all_teams.length > 1">
<div class="border-t border-gray-200 dark:border-gray-600" /> <div class="border-t border-gray-200 dark:border-gray-600" />

View File

@@ -45,7 +45,7 @@ const deleteTeam = () => {
</DangerButton> </DangerButton>
</div> </div>
<!-- Delete Team Confirmation Modal --> <!-- Delete Organization Confirmation Modal -->
<ConfirmationModal :show="confirmingTeamDeletion" @close="confirmingTeamDeletion = false"> <ConfirmationModal :show="confirmingTeamDeletion" @close="confirmingTeamDeletion = false">
<template #title> <template #title>
Delete Team Delete Team

View File

@@ -97,7 +97,7 @@ const displayableRole = (role) => {
<div v-if="userPermissions.canAddTeamMembers"> <div v-if="userPermissions.canAddTeamMembers">
<SectionBorder /> <SectionBorder />
<!-- Add Team Member --> <!-- Add Organization Member -->
<FormSection @submitted="addTeamMember"> <FormSection @submitted="addTeamMember">
<template #title> <template #title>
Add Team Member Add Team Member
@@ -177,7 +177,7 @@ const displayableRole = (role) => {
<div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers"> <div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers">
<SectionBorder /> <SectionBorder />
<!-- Team Member Invitations --> <!-- Organization Member Invitations -->
<ActionSection class="mt-10 sm:mt-0"> <ActionSection class="mt-10 sm:mt-0">
<template #title> <template #title>
Pending Team Invitations Pending Team Invitations
@@ -187,7 +187,7 @@ const displayableRole = (role) => {
These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation. These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation.
</template> </template>
<!-- Pending Team Member Invitation List --> <!-- Pending Organization Member Invitation List -->
<template #content> <template #content>
<div class="space-y-6"> <div class="space-y-6">
<div v-for="invitation in team.team_invitations" :key="invitation.id" class="flex items-center justify-between"> <div v-for="invitation in team.team_invitations" :key="invitation.id" class="flex items-center justify-between">
@@ -196,7 +196,7 @@ const displayableRole = (role) => {
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<!-- Cancel Team Invitation --> <!-- Cancel Organization Invitation -->
<button <button
v-if="userPermissions.canRemoveTeamMembers" v-if="userPermissions.canRemoveTeamMembers"
class="cursor-pointer ms-6 text-sm text-red-500 focus:outline-none" class="cursor-pointer ms-6 text-sm text-red-500 focus:outline-none"
@@ -214,7 +214,7 @@ const displayableRole = (role) => {
<div v-if="team.users.length > 0"> <div v-if="team.users.length > 0">
<SectionBorder /> <SectionBorder />
<!-- Manage Team Members --> <!-- Manage Organization Members -->
<ActionSection class="mt-10 sm:mt-0"> <ActionSection class="mt-10 sm:mt-0">
<template #title> <template #title>
Team Members Team Members
@@ -224,7 +224,7 @@ const displayableRole = (role) => {
All of the people that are part of this team. All of the people that are part of this team.
</template> </template>
<!-- Team Member List --> <!-- Organization Member List -->
<template #content> <template #content>
<div class="space-y-6"> <div class="space-y-6">
<div v-for="user in team.users" :key="user.id" class="flex items-center justify-between"> <div v-for="user in team.users" :key="user.id" class="flex items-center justify-between">
@@ -236,7 +236,7 @@ const displayableRole = (role) => {
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<!-- Manage Team Member Role --> <!-- Manage Organization Member Role -->
<button <button
v-if="userPermissions.canUpdateTeamMembers && availableRoles.length" v-if="userPermissions.canUpdateTeamMembers && availableRoles.length"
class="ms-2 text-sm text-gray-400 underline" class="ms-2 text-sm text-gray-400 underline"
@@ -249,7 +249,7 @@ const displayableRole = (role) => {
{{ displayableRole(user.membership.role) }} {{ displayableRole(user.membership.role) }}
</div> </div>
<!-- Leave Team --> <!-- Leave Organization -->
<button <button
v-if="$page.props.auth.user.id === user.id" v-if="$page.props.auth.user.id === user.id"
class="cursor-pointer ms-6 text-sm text-red-500" class="cursor-pointer ms-6 text-sm text-red-500"
@@ -258,7 +258,7 @@ const displayableRole = (role) => {
Leave Leave
</button> </button>
<!-- Remove Team Member --> <!-- Remove Organization Member -->
<button <button
v-else-if="userPermissions.canRemoveTeamMembers" v-else-if="userPermissions.canRemoveTeamMembers"
class="cursor-pointer ms-6 text-sm text-red-500" class="cursor-pointer ms-6 text-sm text-red-500"
@@ -328,7 +328,7 @@ const displayableRole = (role) => {
</template> </template>
</DialogModal> </DialogModal>
<!-- Leave Team Confirmation Modal --> <!-- Leave Organization Confirmation Modal -->
<ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false"> <ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false">
<template #title> <template #title>
Leave Team Leave Team
@@ -354,7 +354,7 @@ const displayableRole = (role) => {
</template> </template>
</ConfirmationModal> </ConfirmationModal>
<!-- Remove Team Member Confirmation Modal --> <!-- Remove Organization Member Confirmation Modal -->
<ConfirmationModal :show="teamMemberBeingRemoved" @close="teamMemberBeingRemoved = null"> <ConfirmationModal :show="teamMemberBeingRemoved" @close="teamMemberBeingRemoved = null">
<template #title> <template #title>
Remove Team Member Remove Team Member

View File

@@ -35,7 +35,7 @@ const updateTeamName = () => {
</template> </template>
<template #form> <template #form>
<!-- Team Owner Information --> <!-- Organization Owner Information -->
<div class="col-span-6"> <div class="col-span-6">
<InputLabel value="Team Owner" /> <InputLabel value="Team Owner" />
@@ -51,7 +51,7 @@ const updateTeamName = () => {
</div> </div>
</div> </div>
<!-- Team Name --> <!-- Organization Name -->
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Team Name" /> <InputLabel for="name" value="Team Name" />

View File

@@ -1,5 +1,5 @@
@component('mail::message') @component('mail::message')
{{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }} {{ __('You have been invited to join the :team team!', ['team' => $invitation->organization->name]) }}
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration())) @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration()))
{{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }} {{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }}

View File

@@ -20,7 +20,7 @@ class ApiTokenPermissionsTest extends TestCase
$this->markTestSkipped('API support is not enabled.'); $this->markTestSkipped('API support is not enabled.');
} }
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$token = $user->tokens()->create([ $token = $user->tokens()->create([
'name' => 'Test Token', 'name' => 'Test Token',

View File

@@ -19,7 +19,7 @@ class CreateApiTokenTest extends TestCase
$this->markTestSkipped('API support is not enabled.'); $this->markTestSkipped('API support is not enabled.');
} }
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$response = $this->post('/user/api-tokens', [ $response = $this->post('/user/api-tokens', [
'name' => 'Test Token', 'name' => 'Test Token',

View File

@@ -14,13 +14,13 @@ class CreateTeamTest extends TestCase
public function test_teams_can_be_created(): void public function test_teams_can_be_created(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$response = $this->post('/teams', [ $response = $this->post('/teams', [
'name' => 'Test Team', 'name' => 'Test Organization',
]); ]);
$this->assertCount(2, $user->fresh()->ownedTeams); $this->assertCount(2, $user->fresh()->ownedTeams);
$this->assertEquals('Test Team', $user->fresh()->ownedTeams()->latest('id')->first()->name); $this->assertEquals('Test Organization', $user->fresh()->ownedTeams()->latest('id')->first()->name);
} }
} }

View File

@@ -20,7 +20,7 @@ class DeleteApiTokenTest extends TestCase
$this->markTestSkipped('API support is not enabled.'); $this->markTestSkipped('API support is not enabled.');
} }
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$token = $user->tokens()->create([ $token = $user->tokens()->create([
'name' => 'Test Token', 'name' => 'Test Token',

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\Team; use App\Models\Organization;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@@ -15,9 +15,9 @@ class DeleteTeamTest extends TestCase
public function test_teams_can_be_deleted(): void public function test_teams_can_be_deleted(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$user->ownedTeams()->save($team = Team::factory()->make([ $user->ownedTeams()->save($team = Organization::factory()->make([
'personal_team' => false, 'personal_team' => false,
])); ]));
@@ -33,7 +33,7 @@ class DeleteTeamTest extends TestCase
public function test_personal_teams_cant_be_deleted(): void public function test_personal_teams_cant_be_deleted(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$response = $this->delete('/teams/'.$user->currentTeam->id); $response = $this->delete('/teams/'.$user->currentTeam->id);

View File

@@ -23,7 +23,7 @@ class EmailVerificationTest extends TestCase
$this->markTestSkipped('Email verification not enabled.'); $this->markTestSkipped('Email verification not enabled.');
} }
$user = User::factory()->withPersonalTeam()->unverified()->create(); $user = User::factory()->withPersonalOrganization()->unverified()->create();
$response = $this->actingAs($user)->get('/email/verify'); $response = $this->actingAs($user)->get('/email/verify');

View File

@@ -7,7 +7,6 @@ namespace Tests\Feature;
use App\Models\User; use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Laravel\Jetstream\Features;
use Laravel\Jetstream\Mail\TeamInvitation; use Laravel\Jetstream\Mail\TeamInvitation;
use Tests\TestCase; use Tests\TestCase;
@@ -17,41 +16,37 @@ class InviteTeamMemberTest extends TestCase
public function test_team_members_can_be_invited_to_team(): void public function test_team_members_can_be_invited_to_team(): void
{ {
if (! Features::sendsTeamInvitations()) { // Arrange
$this->markTestSkipped('Team invitations not enabled.');
}
Mail::fake(); Mail::fake();
$this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); // Act
$response = $this->post('/teams/'.$user->currentTeam->id.'/members', [ $response = $this->post('/teams/'.$user->currentTeam->id.'/members', [
'email' => 'test@example.com', 'email' => 'test@example.com',
'role' => 'admin', 'role' => 'admin',
]); ]);
// Assert
Mail::assertSent(TeamInvitation::class); Mail::assertSent(TeamInvitation::class);
$this->assertCount(1, $user->currentTeam->fresh()->teamInvitations); $this->assertCount(1, $user->currentTeam->fresh()->teamInvitations);
} }
public function test_team_member_invitations_can_be_cancelled(): void public function test_team_member_invitations_can_be_cancelled(): void
{ {
if (! Features::sendsTeamInvitations()) { // Arrange
$this->markTestSkipped('Team invitations not enabled.');
}
Mail::fake(); Mail::fake();
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$invitation = $user->currentTeam->teamInvitations()->create([ $invitation = $user->currentTeam->teamInvitations()->create([
'email' => 'test@example.com', 'email' => 'test@example.com',
'role' => 'admin', 'role' => 'admin',
]); ]);
// Act
$response = $this->delete('/team-invitations/'.$invitation->id); $response = $this->delete('/team-invitations/'.$invitation->id);
// Assert
$this->assertCount(0, $user->currentTeam->fresh()->teamInvitations); $this->assertCount(0, $user->currentTeam->fresh()->teamInvitations);
} }
} }

View File

@@ -14,7 +14,7 @@ class LeaveTeamTest extends TestCase
public function test_users_can_leave_teams(): void public function test_users_can_leave_teams(): void
{ {
$user = User::factory()->withPersonalTeam()->create(); $user = User::factory()->withPersonalOrganization()->create();
$user->currentTeam->users()->attach( $user->currentTeam->users()->attach(
$otherUser = User::factory()->create(), ['role' => 'admin'] $otherUser = User::factory()->create(), ['role' => 'admin']
@@ -29,7 +29,7 @@ class LeaveTeamTest extends TestCase
public function test_team_owners_cant_leave_their_own_team(): void public function test_team_owners_cant_leave_their_own_team(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id); $response = $this->delete('/teams/'.$user->currentTeam->id.'/members/'.$user->id);

View File

@@ -14,7 +14,7 @@ class PasswordConfirmationTest extends TestCase
public function test_confirm_password_screen_can_be_rendered(): void public function test_confirm_password_screen_can_be_rendered(): void
{ {
$user = User::factory()->withPersonalTeam()->create(); $user = User::factory()->withPersonalOrganization()->create();
$response = $this->actingAs($user)->get('/user/confirm-password'); $response = $this->actingAs($user)->get('/user/confirm-password');

View File

@@ -14,7 +14,7 @@ class RemoveTeamMemberTest extends TestCase
public function test_team_members_can_be_removed_from_teams(): void public function test_team_members_can_be_removed_from_teams(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$user->currentTeam->users()->attach( $user->currentTeam->users()->attach(
$otherUser = User::factory()->create(), ['role' => 'admin'] $otherUser = User::factory()->create(), ['role' => 'admin']
@@ -27,7 +27,7 @@ class RemoveTeamMemberTest extends TestCase
public function test_only_team_owner_can_remove_team_members(): void public function test_only_team_owner_can_remove_team_members(): void
{ {
$user = User::factory()->withPersonalTeam()->create(); $user = User::factory()->withPersonalOrganization()->create();
$user->currentTeam->users()->attach( $user->currentTeam->users()->attach(
$otherUser = User::factory()->create(), ['role' => 'admin'] $otherUser = User::factory()->create(), ['role' => 'admin']

View File

@@ -14,7 +14,7 @@ class UpdateTeamMemberRoleTest extends TestCase
public function test_team_member_roles_can_be_updated(): void public function test_team_member_roles_can_be_updated(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$user->currentTeam->users()->attach( $user->currentTeam->users()->attach(
$otherUser = User::factory()->create(), ['role' => 'admin'] $otherUser = User::factory()->create(), ['role' => 'admin']
@@ -31,7 +31,7 @@ class UpdateTeamMemberRoleTest extends TestCase
public function test_only_team_owner_can_update_team_member_roles(): void public function test_only_team_owner_can_update_team_member_roles(): void
{ {
$user = User::factory()->withPersonalTeam()->create(); $user = User::factory()->withPersonalOrganization()->create();
$user->currentTeam->users()->attach( $user->currentTeam->users()->attach(
$otherUser = User::factory()->create(), ['role' => 'admin'] $otherUser = User::factory()->create(), ['role' => 'admin']

View File

@@ -14,13 +14,13 @@ class UpdateTeamNameTest extends TestCase
public function test_team_names_can_be_updated(): void public function test_team_names_can_be_updated(): void
{ {
$this->actingAs($user = User::factory()->withPersonalTeam()->create()); $this->actingAs($user = User::factory()->withPersonalOrganization()->create());
$response = $this->put('/teams/'.$user->currentTeam->id, [ $response = $this->put('/teams/'.$user->currentTeam->id, [
'name' => 'Test Team', 'name' => 'Test Organization',
]); ]);
$this->assertCount(1, $user->fresh()->ownedTeams); $this->assertCount(1, $user->fresh()->ownedTeams);
$this->assertEquals('Test Team', $user->currentTeam->fresh()->name); $this->assertEquals('Test Organization', $user->currentTeam->fresh()->name);
} }
} }

View File

@@ -5,14 +5,14 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Client; use App\Models\Client;
use App\Models\Team; use App\Models\Organization;
class ClientModelTest extends ModelTestAbstract class ClientModelTest extends ModelTestAbstract
{ {
public function test_it_belongs_to_a_organization(): void public function test_it_belongs_to_a_organization(): void
{ {
// Arrange // Arrange
$organization = Team::factory()->create(); $organization = Organization::factory()->create();
$client = Client::factory()->forOrganization($organization)->create(); $client = Client::factory()->forOrganization($organization)->create();
// Act // Act

View File

@@ -5,16 +5,16 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Client; use App\Models\Client;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
class ProjectModelTest extends ModelTestAbstract class ProjectModelTest extends ModelTestAbstract
{ {
public function test_it_belongs_to_a_organization(): void public function test_it_belongs_to_a_organization(): void
{ {
// Arrange // Arrange
$organization = Team::factory()->create(); $organization = Organization::factory()->create();
$project = Project::factory()->forOrganization($organization)->create(); $project = Project::factory()->forOrganization($organization)->create();
// Act // Act

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Organization;
use App\Models\Tag; use App\Models\Tag;
use App\Models\Team;
class TagModelTest extends ModelTestAbstract class TagModelTest extends ModelTestAbstract
{ {
public function test_it_belongs_to_a_organization(): void public function test_it_belongs_to_a_organization(): void
{ {
// Arrange // Arrange
$organization = Team::factory()->create(); $organization = Organization::factory()->create();
$task = Tag::factory()->forOrganization($organization)->create(); $task = Tag::factory()->forOrganization($organization)->create();
// Act // Act

View File

@@ -4,16 +4,16 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
class TaskModelTest extends ModelTestAbstract class TaskModelTest extends ModelTestAbstract
{ {
public function test_it_belongs_to_a_organization(): void public function test_it_belongs_to_a_organization(): void
{ {
// Arrange // Arrange
$organization = Team::factory()->create(); $organization = Organization::factory()->create();
$task = Task::factory()->forOrganization($organization)->create(); $task = Task::factory()->forOrganization($organization)->create();
// Act // Act

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Tests\Unit\Model; namespace Tests\Unit\Model;
use App\Models\Organization;
use App\Models\Project; use App\Models\Project;
use App\Models\Task; use App\Models\Task;
use App\Models\Team;
use App\Models\TimeEntry; use App\Models\TimeEntry;
use App\Models\User; use App\Models\User;
@@ -30,7 +30,7 @@ class TimeEntryModelTest extends ModelTestAbstract
public function test_it_belongs_to_a_organization(): void public function test_it_belongs_to_a_organization(): void
{ {
// Arrange // Arrange
$organization = Team::factory()->create(); $organization = Organization::factory()->create();
$timeEntry = TimeEntry::factory()->forOrganization($organization)->create(); $timeEntry = TimeEntry::factory()->forOrganization($organization)->create();
// Act // Act