diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 29c1e3dd..5ebb9d55 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -9,10 +9,12 @@ use App\Models\User; use App\Support\Dashboard\CompoundLibraryRankedStudiesQuery; use App\Support\Dashboard\WorkspaceMoleculeAggregates; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Inertia\Inertia; class DashboardController extends Controller @@ -58,6 +60,7 @@ public function dashboard(DashboardIndexRequest $request) if ($workspace !== 'default') { [$workspaceProjects, $workspaceStudies] = $this->workspaceProjectAndStudyLists($user, $workspace); + $workspaceProjects = $this->decorateProjectsForDashboard($workspaceProjects, $user); $hasProjects = $this->scopedProjectsQuery($user, $team)->exists(); $hasSamples = $this->scopedSamplesQuery($user, $team)->exists(); @@ -77,18 +80,21 @@ public function dashboard(DashboardIndexRequest $request) } $projectsQuery = $this->scopedProjectsQuery($user, $team) - ->with(['users', 'owner', 'tags', 'draft']) + ->with($this->dashboardProjectEagerLoads()) ->orderByDesc('updated_at'); $this->applyStatusFilter($projectsQuery, $filters['projects_status']); $this->applySearchToProjects($projectsQuery, $filters['projects_q']); - $projects = $projectsQuery->paginate( - $filters['projects_per_page'], - ['*'], - 'projects_page', - $filters['projects_page'] - )->withQueryString(); + $projects = $this->decorateProjectsForDashboard( + $projectsQuery->paginate( + $filters['projects_per_page'], + ['*'], + 'projects_page', + $filters['projects_page'] + )->withQueryString(), + $user + ); $samplesQuery = $this->scopedSamplesQuery($user, $team) ->with([ @@ -134,7 +140,7 @@ protected function workspaceProjectAndStudyLists(User $user, string $workspace): { return match ($workspace) { 'shared' => [ - $user->sharedProjects()->with(['owner', 'tags', 'draft'])->get()->all(), + $user->sharedProjects()->with($this->dashboardProjectEagerLoads())->get()->all(), $user->sharedStudies()->with(['owner', 'sample.molecules'])->get()->all(), ], 'recent' => [ @@ -145,7 +151,7 @@ protected function workspaceProjectAndStudyLists(User $user, string $workspace): Project::query() ->where('is_deleted', false) ->whereHasBookmark($user) - ->with(['owner', 'tags', 'draft']) + ->with($this->dashboardProjectEagerLoads()) ->get() ->all(), Study::query() @@ -159,7 +165,7 @@ protected function workspaceProjectAndStudyLists(User $user, string $workspace): Project::query() ->where('owner_id', $user->id) ->where('is_deleted', true) - ->with(['owner', 'tags', 'draft']) + ->with($this->dashboardProjectEagerLoads()) ->get() ->all(), [], @@ -173,11 +179,12 @@ protected function workspaceProjectAndStudyLists(User $user, string $workspace): */ protected function recentProjectsForUser(User $user): array { - $projects = $user->activeProjects()->with(['owner', 'tags', 'draft'])->get(); + $eagerLoads = $this->dashboardProjectEagerLoads(); + $projects = $user->activeProjects()->with($eagerLoads)->get(); foreach ($user->allTeams() as $teamModel) { $projects = $projects->concat( - $teamModel->activeProjects()->with(['owner', 'tags', 'draft'])->get() + $teamModel->activeProjects()->with($eagerLoads)->get() ); } @@ -218,6 +225,49 @@ function (QueryBuilder $query) use ($user, $team): void { ); } + /** + * @return array + */ + protected function dashboardProjectEagerLoads(): array + { + return [ + 'users', + 'owner', + 'tags', + 'draft', + 'projectInvitations', + 'team.owner', + 'team.users', + ]; + } + + /** + * @param LengthAwarePaginator|array|Collection $projects + * @return LengthAwarePaginator|array|Collection + */ + protected function decorateProjectsForDashboard(LengthAwarePaginator|array|Collection $projects, User $user): LengthAwarePaginator|array|Collection + { + $decorate = function (Project $project) use ($user): Project { + $project->setAttribute('viewer_role', $project->userProjectRole($user->email)); + + return $project; + }; + + if ($projects instanceof LengthAwarePaginator) { + $projects->setCollection( + $projects->getCollection()->map($decorate) + ); + + return $projects; + } + + if ($projects instanceof Collection) { + return $projects->map($decorate); + } + + return array_map($decorate, $projects); + } + protected function applyStatusFilter(Builder $query, string $status): void { if ($status !== 'all') { diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 314c0639..6a05ce49 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -158,40 +158,49 @@ public function review(Request $request, $obfuscationCode, GetLicense $getLicens { $project = Project::where([['is_archived', false], ['obfuscationcode', $obfuscationCode]])->firstOrFail(); $project->load('projectInvitations', 'tags', 'authors', 'citations', 'owner'); - if (! $project->is_public) { - $license = null; - if ($project->license_id) { - $license = $getLicense->getLicensebyId($project->license_id); - } - return Inertia::render('Project/Show', [ - 'project' => $project, - 'team' => null, - 'members' => $project->allUsers(), - 'availableRoles' => array_values(Jetstream::$roles), - 'role' => 'reviewer', - 'teamRole' => null, - 'license' => $license, - 'projectPermissions' => [ - 'canDeleteProject' => false, - 'canUpdateProject' => false, - ], - 'preview' => true, - ]); - } else { - $identifier = explode(':', $project->identifier)[1]; + if ($project->is_public) { + $rawIdentifier = $project->getRawOriginal('identifier'); + if ($rawIdentifier !== null && $rawIdentifier !== '') { + $targetUrl = route('public.project.id', ['id' => 'P'.$rawIdentifier]); + $query = $request->query(); + if ($query !== []) { + $targetUrl .= '?'.http_build_query($query); + } - return redirect()->route('public', $identifier); + return redirect()->to($targetUrl); + } + } + + $license = null; + if ($project->license_id) { + $license = $getLicense->getLicensebyId($project->license_id); } + return Inertia::render('Project/Show', [ + 'project' => $project, + 'team' => null, + 'members' => $project->allUsers(), + 'availableRoles' => array_values(Jetstream::$roles), + 'role' => 'reviewer', + 'teamRole' => null, + 'license' => $license, + 'projectPermissions' => [ + 'canDeleteProject' => false, + 'canUpdateProject' => false, + ], + 'preview' => true, + ]); } public function reviewerStudies(Request $request, $obfuscationCode) { - $project = Project::where([['is_archived', false], ['obfuscationcode', $obfuscationCode]])->firstOrFail(); - if ($project) { - return StudyResource::collection(Study::where('project_id', $project->id)->filter($request->only('search', 'sort', 'mode'))->paginate(9)->withQueryString()); - } + $project = Project::where([ + ['is_archived', false], + ['obfuscationcode', $obfuscationCode], + ])->firstOrFail(); + + return $this->projectStudiesResponse($request, $project, publicOnly: false); } public function studies(Request $request, Project $project) diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index 31fd6f21..439bf99b 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -790,7 +790,7 @@ const navigationSections = [ { auth: false, name: "Spectra Library", - href: "/compounds", + href: "/search?scope=compounds", icon: SwatchIcon, }, ], diff --git a/resources/js/Mixins/Global.js b/resources/js/Mixins/Global.js index 408ef7e0..d7f8a9d7 100644 --- a/resources/js/Mixins/Global.js +++ b/resources/js/Mixins/Global.js @@ -163,16 +163,18 @@ export default { }).format(date); }, /** - * Unified date+time display for record metadata (Published / Created / Updated rows). + * Unified date display for record metadata (Published / Created / Updated rows). */ formatRecordTimestamp(timestamp) { if (!timestamp) { return ""; } const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) { + return ""; + } return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", - timeStyle: "short", }).format(date); }, md(data) { diff --git a/resources/js/Pages/Project/Index.vue b/resources/js/Pages/Project/Index.vue index 18ad4b5e..b9b1b6f0 100644 --- a/resources/js/Pages/Project/Index.vue +++ b/resources/js/Pages/Project/Index.vue @@ -154,7 +154,10 @@ aria-hidden="true" /> -
+
+
- {{ dashboardDoiHeadingLabel(project) }} - -
- - {{ dashboardDoiLinkText(project) }} - -
+ People + +
+ +
+
+
+
+ People +
+
+ +
+
+