Separate dashboard functions from grouped administration operations
tests / ci (push) Waiting to run
tests / ci (push) Waiting to run
This commit is contained in:
@@ -57,6 +57,7 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'administrationNavigation' => app(ContentManagement::class)->administrationNavigation($request->user(), $contentManagementAccess),
|
||||
'dashboardNavigation' => app(ContentManagement::class)->dashboardNavigation($request->user(), $contentManagementAccess),
|
||||
'procurementEnabled' => app(Procurement::class)->available(),
|
||||
'fundingEnabled' => app(FundingProgrammes::class)->installed(),
|
||||
|
||||
@@ -54,25 +54,18 @@ final class ContentManagement
|
||||
/** @param array<array-key,mixed> $access
|
||||
* @return list<array{label:string,href:string}>
|
||||
*/
|
||||
public function dashboardNavigation(?User $actor, array $access): array
|
||||
public function administrationNavigation(?User $actor, array $access): array
|
||||
{
|
||||
$workflow = app(PortalEditorialWorkflow::class);
|
||||
if (! $actor || ! $workflow->available() || ! $workflow->canEnter($actor)) {
|
||||
if (! $actor || ! $actor->hasVerifiedEmail()) {
|
||||
return [];
|
||||
}
|
||||
$items = [['label' => 'Visão geral', 'href' => '/management/editorial']];
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
if ($documents->full($actor) || $documents->editorial($actor)) {
|
||||
foreach (['article' => 'Artigos', 'legislation' => 'Legislação', 'document' => 'Documentos'] as $kind => $label) {
|
||||
$items[] = ['label' => $label, 'href' => '/management/editorial?kind='.$kind];
|
||||
}
|
||||
}
|
||||
foreach (['opportunities' => 'Oportunidades', 'clusters' => 'Competitividade', 'portfolio' => 'Carteira', 'demography' => 'Demografia'] as $key => $label) {
|
||||
$items = [];
|
||||
foreach (['documents' => 'Biblioteca e documentos', 'opportunities' => 'Oportunidades', 'clusters' => 'Competitividade', 'portfolio' => 'Carteira', 'demography' => 'Demografia', 'procurement' => 'Concursos e anúncios'] as $key => $label) {
|
||||
if (isset($access[$key]['index'])) {
|
||||
$items[] = ['label' => $label, 'href' => $access[$key]['index']];
|
||||
}
|
||||
}
|
||||
if (app(PortalEditorialSubjects::class)->available() && $actor->can('manage-portal')) {
|
||||
if (app(PortalEditorialSubjects::class)->available() && $actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')) {
|
||||
$items[] = ['label' => 'Estruturas e relações', 'href' => '/management/editorial/subjects'];
|
||||
}
|
||||
$native = app(PortalEditorialNativeNotices::class);
|
||||
@@ -83,7 +76,38 @@ final class ContentManagement
|
||||
$items[] = ['label' => 'Zonas e indicadores', 'href' => '/management/investment/zones'];
|
||||
}
|
||||
|
||||
$items[] = ['label' => 'Notificações', 'href' => '/management/editorial#notificacoes'];
|
||||
return $items;
|
||||
}
|
||||
|
||||
/** @param array<array-key,mixed> $access
|
||||
* @return list<array{label:string,href:string}>
|
||||
*/
|
||||
public function dashboardNavigation(?User $actor, array $access): array
|
||||
{
|
||||
if (! $actor || ! $actor->hasVerifiedEmail() || (! $actor->can('manage-portal') && $access === [] && ! app(ScopedPortalAdministration::class)->administrative($actor))) {
|
||||
return [];
|
||||
}
|
||||
$items = [['label' => 'Visão geral', 'href' => '/dashboard'], ['label' => 'O meu trabalho', 'href' => '/dashboard#meu-trabalho']];
|
||||
if ($actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')) {
|
||||
foreach (['manifestacoes' => 'Manifestações de interesse', 'indicadores-oportunidades' => 'Indicadores de oportunidades', 'pedidos-recentes' => 'Pedidos recentes', 'accoes-rapidas' => 'Acções rápidas'] as $anchor => $label) {
|
||||
$items[] = ['label' => $label, 'href' => '/dashboard#'.$anchor];
|
||||
}
|
||||
$items[] = ['label' => 'Indicadores de utilização', 'href' => '/management/product-usage'];
|
||||
}
|
||||
if (! $actor->can('manage-portal') && app(InvestorPath::class)->available()) {
|
||||
$items[] = ['label' => 'Os meus investimentos', 'href' => '/dashboard#meus-investimentos'];
|
||||
}
|
||||
$workflow = app(PortalEditorialWorkflow::class);
|
||||
if ($workflow->available() && $workflow->canEnter($actor)) {
|
||||
$items[] = ['label' => 'Circuito editorial', 'href' => '/management/editorial'];
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
if ($documents->full($actor) || $documents->editorial($actor)) {
|
||||
foreach (['article' => 'Revisão de artigos', 'legislation' => 'Revisão de legislação', 'document' => 'Revisão de documentos'] as $kind => $label) {
|
||||
$items[] = ['label' => $label, 'href' => '/management/editorial?kind='.$kind];
|
||||
}
|
||||
}
|
||||
$items[] = ['label' => 'Notificações editoriais', 'href' => '/management/editorial#notificacoes'];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
## 14/11/2026 — correcção da hierarquia Administração → Dashboard
|
||||
|
||||
Clarificação expressa da Tempo: agrupar **todas as operações administrativas em Administração** e reservar **Dashboard** às funções dos painéis. A apresentação anterior misturava os cadastros do circuito editorial com o dashboard principal e conservava atalhos administrativos dentro de Mais. Esta correcção não acrescenta funcionalidades ou permissões.
|
||||
|
||||
Hierarquia comum ao cabeçalho e à navegação lateral, com grupos omitidos quando não têm destinos autorizados:
|
||||
|
||||
- **Administração → Dashboard:** Visão geral do painel `/dashboard`; O meu trabalho; manifestações, indicadores, pedidos recentes e acções rápidas quando o painel administrativo já os permite; circuito editorial, filas de revisão e notificações segundo os âmbitos existentes. Os investimentos próprios mantêm o contexto do painel aplicável. Os filtros e âncoras seleccionam a função actual.
|
||||
- **Administração → Conteúdos e oportunidades:** gestão de conteúdos, documentos, oportunidades, preparação de análises e concursos autorizados.
|
||||
- **Administração → Cadastros e investimento:** sectores, projectos, clusters, carteira, estruturas/relações, demografia, território, zonas, referências e educação, conforme a capacidade existente.
|
||||
- **Administração → Pedidos e acompanhamento:** interesses, CRM e atribuições autorizadas.
|
||||
- **Administração → Acessos e configuração:** gestão/auditoria de perfis e configurações já autorizadas.
|
||||
|
||||
Os cadastros deixam de figurar dentro de Dashboard; atalhos administrativos deixam de estar dispersos em Mais. Os destinos anteriores são preservados uma vez, incluindo funções de perfis antes em Área de trabalho. Funções pessoais e acessos públicos conservam a sua área. O painel `/dashboard` é a visão geral; `/management/editorial` volta a identificar-se como **Circuito editorial** e mantém os cartões/revisões existentes. Não se importam estados a partir de componentes de página; a correcção do manifesto permanece intacta.
|
||||
|
||||
QA: navegação/cabeçalho/sidebar **25 testes**, apresentação/secções **4**, acesso/inbox **4/35**, abertura HTML autenticada dos dois painéis com build real **1/11**, análise estática, tipos e compilação aprovados. Provas em `outputs/administration-dashboard-hierarchy-20261114`. Testa-se a hierarquia exacta, ausência de cadastros dentro de Dashboard, remoção das operações administrativas de Mais, preservação de destinos e âncoras reais. Instalação correctiva pelo processo Git existente com backup/restauro em curso; não declarar concluída até conferir a revisão servida. A conferência visual continua pendente pelo conector indisponível.
|
||||
|
||||
## 14/11/2026 — erro 500 corrigido e correcção instalada em dd20e747
|
||||
|
||||
A revisão `dd20e747036d45892faf3273a44e385132668ee4` foi promovida pelo executor Git para reparar a entrega do menu/dashboard. O manifesto servido volta a conter `resources/js/pages/management/editorial-workflow.tsx`. A geração do HTML completo no servidor com o template/manifesto reais e conteúdo exclusivamente sintético passou (200); o respectivo ficheiro compilado respondeu 200 por HTTPS. Não se simularam credenciais ou sessões privadas no destino. A abertura HTTP autenticada com o build real passou em QA (1/8), além dos 12 testes frontend focados. O executor aprovou **1911 testes de interface**, incluindo o novo controlo do manifesto, 132 unitários/611 asserções, estrutura 1/3106, tipos e compilação.
|
||||
|
||||
@@ -33,6 +33,7 @@ export function AppSidebar() {
|
||||
profileAccess,
|
||||
scopedAdministration,
|
||||
dashboardNavigation,
|
||||
administrationNavigation,
|
||||
} = usePage<{
|
||||
auth: Auth;
|
||||
existingBusinessEnabled?: boolean;
|
||||
@@ -43,6 +44,7 @@ export function AppSidebar() {
|
||||
contentManagementHref?: string | null;
|
||||
scopedAdministration?: { label: string; href: string }[];
|
||||
dashboardNavigation?: { label: string; href: string }[];
|
||||
administrationNavigation?: { label: string; href: string }[];
|
||||
contentManagementAccess?: {
|
||||
opportunities?: { index: string; create?: string | null };
|
||||
};
|
||||
@@ -59,6 +61,7 @@ export function AppSidebar() {
|
||||
contentManagementAccess,
|
||||
scopedAdministration,
|
||||
dashboardNavigation,
|
||||
administrationNavigation,
|
||||
legacyContent: currentPage.props.legacyContentEnabled as
|
||||
| boolean
|
||||
| undefined,
|
||||
|
||||
@@ -54,6 +54,7 @@ export function PortalGroupedMenu() {
|
||||
opportunityManagement?: OpportunityManagementAccess | null;
|
||||
scopedAdministration?: { label: string; href: string }[];
|
||||
dashboardNavigation?: { label: string; href: string }[];
|
||||
administrationNavigation?: { label: string; href: string }[];
|
||||
contentManagementHref?: string | null;
|
||||
contentManagementAccess?: {
|
||||
opportunities?: { index: string; create?: string | null };
|
||||
@@ -81,6 +82,7 @@ export function PortalGroupedMenu() {
|
||||
contentManagementAccess: props.contentManagementAccess,
|
||||
scopedAdministration: props.scopedAdministration,
|
||||
dashboardNavigation: props.dashboardNavigation,
|
||||
administrationNavigation: props.administrationNavigation,
|
||||
});
|
||||
const current = currentNavigationGroup(groups, url);
|
||||
const currentItem = currentNavigationItem(groups, url);
|
||||
|
||||
@@ -22,8 +22,11 @@ export function PortalSidebarNavigation({
|
||||
}) {
|
||||
const active = currentNavigationItem(groups, url);
|
||||
const ordered = [
|
||||
...groups.filter((group) => group.id === 'administration'),
|
||||
...groups.filter((group) => group.id === 'work'),
|
||||
...groups.filter((group) => group.id !== 'work'),
|
||||
...groups.filter(
|
||||
(group) => group.id !== 'work' && group.id !== 'administration',
|
||||
),
|
||||
];
|
||||
const links = (items: PortalNavItem[], depth = 0): ReactNode =>
|
||||
items.map((item) =>
|
||||
|
||||
@@ -14,6 +14,7 @@ export type PortalNavAccess = {
|
||||
};
|
||||
scopedAdministration?: { label: string; href: string }[];
|
||||
dashboardNavigation?: { label: string; href: string }[];
|
||||
administrationNavigation?: { label: string; href: string }[];
|
||||
procurement?: boolean;
|
||||
funding?: boolean;
|
||||
authenticated: boolean;
|
||||
@@ -422,7 +423,9 @@ export function currentNavigationGroup(
|
||||
for (const [key, value] of target.searchParams)
|
||||
if (current.searchParams.get(key) !== value) return;
|
||||
const score =
|
||||
target.pathname.length + target.searchParams.size * 100;
|
||||
target.pathname.length +
|
||||
target.searchParams.size * 100 +
|
||||
(target.hash ? 10000 : 0);
|
||||
if (!match || score > match.score) match = { group, score };
|
||||
});
|
||||
for (const group of groups) visit(group.children ?? [], group.id);
|
||||
@@ -449,7 +452,7 @@ export function currentNavigationItem(
|
||||
)
|
||||
)
|
||||
continue;
|
||||
const score = target.searchParams.size;
|
||||
const score = target.searchParams.size + (target.hash ? 10000 : 0);
|
||||
if (score > best.score) {
|
||||
best.id = item.id;
|
||||
best.score = score;
|
||||
@@ -479,43 +482,125 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
: null)
|
||||
: null;
|
||||
const dashboardActivities = access.authenticated
|
||||
? [
|
||||
...(access.dashboardNavigation ?? []),
|
||||
...previous
|
||||
? access.dashboardNavigation?.length
|
||||
? access.dashboardNavigation
|
||||
: previous
|
||||
.filter((group) => group.id === 'account')
|
||||
.flatMap((group) => group.children ?? [])
|
||||
.filter((item) => item.href === '/dashboard')
|
||||
.map((item) => ({
|
||||
label: 'Painel operacional',
|
||||
href: item.href!,
|
||||
})),
|
||||
]
|
||||
.map((item) => ({ label: 'Visão geral', href: item.href! }))
|
||||
: [];
|
||||
const administration = access.authenticated
|
||||
const flatten = (items: PortalNavItem[]): PortalNavItem[] =>
|
||||
items.flatMap((item) =>
|
||||
item.children ? flatten(item.children) : [item],
|
||||
);
|
||||
const profileAdministration = new Set([
|
||||
'profile-administration',
|
||||
'profile-installation',
|
||||
'profile-audit',
|
||||
]);
|
||||
const candidates = access.authenticated
|
||||
? [
|
||||
...(dashboardActivities.length
|
||||
...(workHub
|
||||
? [link('work-entry', 'Gestão de conteúdos', workHub)]
|
||||
: []),
|
||||
...(workCreate
|
||||
? [
|
||||
{
|
||||
id: 'collaborative-dashboard',
|
||||
label: 'Dashboard',
|
||||
children: dashboardActivities.map((item, index) =>
|
||||
link(
|
||||
'dashboard-activity-' + index,
|
||||
item.label,
|
||||
item.href,
|
||||
),
|
||||
),
|
||||
},
|
||||
link(
|
||||
'prepare-analysis',
|
||||
'Preparar análise e ideia',
|
||||
workCreate + '?analysis=proactive',
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...previous
|
||||
.filter((group) => group.id === 'management')
|
||||
.flatMap((group) => group.children ?? []),
|
||||
...(workOpportunities
|
||||
? [
|
||||
link(
|
||||
'work-opportunities',
|
||||
'Oportunidades em preparação',
|
||||
workOpportunities.index,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(access.administrationNavigation ?? []).map((item, index) =>
|
||||
link(
|
||||
'administrative-activity-' + index,
|
||||
item.label,
|
||||
item.href,
|
||||
),
|
||||
),
|
||||
...flatten(
|
||||
previous
|
||||
.filter((group) => group.id === 'management')
|
||||
.flatMap((group) => group.children ?? []),
|
||||
),
|
||||
...(access.scopedAdministration ?? []).map((item, index) =>
|
||||
link('scoped-' + index, item.label, item.href),
|
||||
),
|
||||
...previous
|
||||
.filter((group) => group.id === 'profile-opportunities')
|
||||
.flatMap((group) => group.children ?? [])
|
||||
.filter((item) => profileAdministration.has(item.id)),
|
||||
]
|
||||
: [];
|
||||
const categories = [
|
||||
{
|
||||
id: 'administration-content',
|
||||
label: 'Conteúdos e oportunidades',
|
||||
children: [] as PortalNavItem[],
|
||||
},
|
||||
{
|
||||
id: 'administration-records',
|
||||
label: 'Cadastros e investimento',
|
||||
children: [] as PortalNavItem[],
|
||||
},
|
||||
{
|
||||
id: 'administration-followup',
|
||||
label: 'Pedidos e acompanhamento',
|
||||
children: [] as PortalNavItem[],
|
||||
},
|
||||
{
|
||||
id: 'administration-access',
|
||||
label: 'Acessos e configuração',
|
||||
children: [] as PortalNavItem[],
|
||||
},
|
||||
];
|
||||
for (const item of candidates) {
|
||||
const path = new URL(item.href!, 'https://portal.invalid').pathname;
|
||||
const category =
|
||||
/^\/(gestao-conteudos|management\/(portal-services|opportunities|concursos)|minhas-oportunidades)(\/|$)/.test(
|
||||
path,
|
||||
)
|
||||
? 0
|
||||
: /^\/management\/(interests|crm|balcao\/atribuicoes)(\/|$)/.test(
|
||||
path,
|
||||
)
|
||||
? 2
|
||||
: profileAdministration.has(item.id) ||
|
||||
/-install$/.test(path) ||
|
||||
path === '/management/ai'
|
||||
? 3
|
||||
: 1;
|
||||
categories[category].children.push(item);
|
||||
}
|
||||
const administration = [
|
||||
...(dashboardActivities.length
|
||||
? [
|
||||
{
|
||||
id: 'collaborative-dashboard',
|
||||
label: 'Dashboard',
|
||||
children: dashboardActivities.map((item, index) =>
|
||||
link(
|
||||
'dashboard-activity-' + index,
|
||||
item.label,
|
||||
item.href,
|
||||
),
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...categories.filter((group) => group.children.length > 0),
|
||||
];
|
||||
return uniqueNavigationDestinations([
|
||||
{
|
||||
id: 'discovery',
|
||||
@@ -655,34 +740,13 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
id: 'work',
|
||||
label: 'Área de trabalho',
|
||||
children: [
|
||||
link(
|
||||
'work-entry',
|
||||
workHub
|
||||
? 'Gestão de conteúdos e oportunidades'
|
||||
: 'Acesso à gestão',
|
||||
workHub || '/acesso-gestao',
|
||||
),
|
||||
...(workCreate
|
||||
? [
|
||||
link(
|
||||
'prepare-analysis',
|
||||
'Preparar análise e ideia',
|
||||
workCreate + '?analysis=proactive',
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(workOpportunities
|
||||
? [
|
||||
link(
|
||||
'work-opportunities',
|
||||
'Oportunidades em preparação',
|
||||
workOpportunities.index,
|
||||
),
|
||||
]
|
||||
...(!workHub
|
||||
? [link('work-entry', 'Acesso à gestão', '/acesso-gestao')]
|
||||
: []),
|
||||
...previous
|
||||
.filter((group) => group.id === 'profile-opportunities')
|
||||
.flatMap((group) => group.children ?? []),
|
||||
.flatMap((group) => group.children ?? [])
|
||||
.filter((item) => !profileAdministration.has(item.id)),
|
||||
],
|
||||
},
|
||||
...previous.filter((group) => group.id === 'account'),
|
||||
|
||||
@@ -60,7 +60,20 @@ export default function Dashboard({
|
||||
<h1 className="text-2xl font-semibold">Painel do portal</h1>
|
||||
<ProfileNavigation />
|
||||
<ContentManagementEntry />
|
||||
<DashboardWorkQueue queue={workQueue} />
|
||||
<section id="meu-trabalho" className="scroll-mt-24">
|
||||
{workQueue ? (
|
||||
<DashboardWorkQueue queue={workQueue} />
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold">
|
||||
O meu trabalho
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Sem fila de trabalho disponível neste âmbito.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
{editorialQueue && (
|
||||
<section
|
||||
className="rounded-lg border p-4"
|
||||
@@ -128,7 +141,10 @@ export default function Dashboard({
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
<section className="space-y-4">
|
||||
<section
|
||||
id="manifestacoes"
|
||||
className="scroll-mt-24 space-y-4"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">
|
||||
Manifestações de interesse
|
||||
</h2>
|
||||
@@ -149,7 +165,10 @@ export default function Dashboard({
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="space-y-4">
|
||||
<section
|
||||
id="indicadores-oportunidades"
|
||||
className="scroll-mt-24 space-y-4"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">
|
||||
<Link href="/management/opportunities">
|
||||
{opportunityManagement
|
||||
@@ -190,7 +209,10 @@ export default function Dashboard({
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="space-y-4">
|
||||
<section
|
||||
id="pedidos-recentes"
|
||||
className="scroll-mt-24 space-y-4"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h2 className="text-lg font-semibold">
|
||||
Pedidos mais recentes
|
||||
@@ -243,6 +265,7 @@ export default function Dashboard({
|
||||
)}
|
||||
</section>
|
||||
<nav
|
||||
id="accoes-rapidas"
|
||||
aria-label="Acções rápidas"
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
@@ -264,7 +287,10 @@ export default function Dashboard({
|
||||
</nav>
|
||||
</>
|
||||
) : investorPath ? (
|
||||
<section className="space-y-4 rounded-xl border p-6">
|
||||
<section
|
||||
id="meus-investimentos"
|
||||
className="scroll-mt-24 space-y-4 rounded-xl border p-6"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">
|
||||
Os meus investimentos
|
||||
</h2>
|
||||
|
||||
@@ -152,7 +152,7 @@ export default function EditorialWorkflow({
|
||||
},
|
||||
].filter((area) => area.entries.some((item) => item.href));
|
||||
return (
|
||||
<Management title="Dashboard">
|
||||
<Management title="Circuito editorial">
|
||||
<p className="max-w-3xl text-sm text-slate-600">
|
||||
Acompanhe a preparação, revisão e publicação numa única área de
|
||||
trabalho. Seleccione uma actividade para continuar nas tarefas
|
||||
|
||||
@@ -25,6 +25,9 @@ final class EditorialDashboardBuildTest extends TestCase
|
||||
$actor = User::factory()->create();
|
||||
config(['portal.managers' => [$actor->email]]);
|
||||
$this->actingAsWithMfa($actor);
|
||||
$nativeKey = 'resources/js/pages/dashboard.tsx';
|
||||
$this->assertArrayHasKey($nativeKey, $manifest);
|
||||
$this->get('/dashboard', ['Accept' => 'text/html'])->assertOk()->assertSee($manifest[$nativeKey]['file'], false);
|
||||
foreach (['/management/editorial', '/management/editorial?kind=legislation'] as $url) {
|
||||
$this->get($url, ['Accept' => 'text/html'])->assertOk()->assertSee('data-page=', false)->assertSee($manifest[$key]['file'], false);
|
||||
}
|
||||
|
||||
@@ -39,13 +39,17 @@ final class EditorialNativeNoticeTest extends TestCase
|
||||
{
|
||||
$service = app(ContentManagement::class);
|
||||
$items = $service->dashboardNavigation($this->author, $service->access($this->author));
|
||||
$this->assertSame(['label' => 'Visão geral', 'href' => '/management/editorial'], $items[0]);
|
||||
$this->assertContains('/management/clusters', array_column($items, 'href'));
|
||||
$this->assertSame(['label' => 'Visão geral', 'href' => '/dashboard'], $items[0]);
|
||||
$this->assertNotContains('/management/clusters', array_column($items, 'href'));
|
||||
$this->assertContains('/management/clusters', array_column($service->administrationNavigation($this->author, $service->access($this->author)), 'href'));
|
||||
$this->assertContains('/dashboard#manifestacoes', array_column($items, 'href'));
|
||||
$outsider = User::factory()->create();
|
||||
$this->assertSame([], $service->dashboardNavigation($outsider, []));
|
||||
$this->assertSame([], $service->dashboardNavigation(null, []));
|
||||
config(['editorial_workflow.enabled' => false]);
|
||||
$this->assertSame([], $service->dashboardNavigation($this->author, $service->access($this->author)));
|
||||
$after = array_column($service->dashboardNavigation($this->author, $service->access($this->author)), 'href');
|
||||
$this->assertContains('/dashboard', $after);
|
||||
$this->assertNotContains('/management/editorial', $after);
|
||||
}
|
||||
|
||||
public function test_cluster_events_use_the_common_inbox_and_recheck_current_scope_and_event_integrity(): void
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import React from 'react';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import ts from 'typescript';
|
||||
const require=createRequire(import.meta.url),module={exports:{}};
|
||||
const code=ts.transpileModule(fs.readFileSync(new URL('../../resources/js/pages/dashboard.tsx',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText;
|
||||
new Function('require','module','exports',code)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({children,href,...props})=>React.createElement('a',{href,...props},children)}:name==='react/jsx-runtime'?require(name):name==='@/routes'?{dashboard:()=>'/dashboard'}:{default:()=>null},module,module.exports);
|
||||
test('each manager dashboard menu anchor resolves to a real section',()=>{
|
||||
const html=renderToStaticMarkup(React.createElement(module.exports.default,{summary:{requestsByStatus:{},publishedOpportunities:0,draftOpportunities:0,activeSectors:0,recentRequests:[]},workQueue:null}));
|
||||
for(const id of ['meu-trabalho','manifestacoes','indicadores-oportunidades','pedidos-recentes','accoes-rapidas'])assert.match(html,new RegExp('id="'+id+'"'));
|
||||
assert.match(html,/Painel do portal/);
|
||||
});
|
||||
test('investor section remains distinct from administrative summary',()=>{
|
||||
const html=renderToStaticMarkup(React.createElement(module.exports.default,{summary:null,investorPath:{contexts:[],current:null}}));
|
||||
assert.match(html,/id="meus-investimentos"/);assert.doesNotMatch(html,/id="manifestacoes"/);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ void test('sidebar renders every nested authorised destination instead of a firs
|
||||
'/management/economic-references/lot-01',
|
||||
])
|
||||
assert.ok(hrefs.includes(href), href);
|
||||
assert.match(html, /Área de trabalho/);
|
||||
assert.match(html, /Administração/);
|
||||
});
|
||||
void test('nested current destination is selected without changing its URL or replacing it with a group root', () => {
|
||||
const { html } = render(
|
||||
|
||||
@@ -226,8 +226,8 @@ void test('work area exposes existing content capability even when the old dashb
|
||||
},
|
||||
},
|
||||
});
|
||||
const work = groups.find((g) => g.id === 'work');
|
||||
assert.equal(work.label, 'Área de trabalho');
|
||||
const work = groups.find((g) => g.id === 'administration').children.find(g => g.id === 'administration-content');
|
||||
assert.equal(work.label, 'Conteúdos e oportunidades');
|
||||
assert.ok(
|
||||
work.children.some(
|
||||
(i) =>
|
||||
@@ -314,7 +314,7 @@ void test('direct analysis and ordinary creation preserve distinct selection sta
|
||||
groups,
|
||||
'/minhas-oportunidades/create?analysis=proactive',
|
||||
),
|
||||
'work',
|
||||
'administration',
|
||||
);
|
||||
assert.equal(
|
||||
currentNavigationItem(
|
||||
@@ -450,15 +450,28 @@ void test('installed concours has its own primary group and preserves public and
|
||||
});
|
||||
|
||||
void test('administration owns dashboard activities once and remains hidden without authorised destinations', () => {
|
||||
const dashboardNavigation = [{label: 'Visão geral', href: '/management/editorial'}, {label: 'Legislação', href: '/management/editorial?kind=legislation'}, {label: 'Competitividade', href: '/management/clusters'}];
|
||||
const groups = portalNavigation({authenticated: true, manager: true, dashboardNavigation});
|
||||
const dashboardNavigation = [{label: 'Visão geral', href: '/dashboard'}, {label: 'O meu trabalho', href: '/dashboard#meu-trabalho'}, {label: 'Circuito editorial', href: '/management/editorial'}];
|
||||
const groups = portalNavigation({authenticated: true, manager: true, dashboardNavigation, administrationNavigation: [{label:'Competitividade',href:'/management/clusters'}]});
|
||||
const admin = groups.find(g => g.id === 'administration');
|
||||
const dashboard = admin.children.find(g => g.id === 'collaborative-dashboard');
|
||||
assert.equal(dashboard.label, 'Dashboard');
|
||||
assert.equal(destinations(dashboard.children).includes('/management/clusters'), false);
|
||||
assert.equal(currentNavigationItem(groups, '/dashboard#meu-trabalho'), 'dashboard-activity-1');
|
||||
assert.deepEqual(destinations(dashboard.children), dashboardNavigation.map(i => i.href));
|
||||
assert.equal(currentNavigationGroup(groups, '/management/editorial?kind=legislation'), 'administration');
|
||||
assert.equal(currentNavigationGroup(groups, '/management/editorial'), 'administration');
|
||||
assert.equal(currentNavigationGroup(groups, '/management/clusters/synthetic/edit'), 'administration');
|
||||
assert.equal(destinations(groups).filter(h => h === '/management/clusters').length, 1);
|
||||
assert.equal(portalNavigation({authenticated: false, manager: false, dashboardNavigation}).some(g => g.id === 'administration'), false);
|
||||
assert.equal(portalNavigation({authenticated: true, manager: false}).some(g => g.id === 'administration'), false);
|
||||
});
|
||||
|
||||
void test('administrative operations are grouped outside Dashboard and removed from More/work', () => {
|
||||
const groups=portalNavigation({authenticated:true,manager:true,contentManagementHref:'/gestao-conteudos',dashboardNavigation:[{label:'Visão geral',href:'/dashboard'},{label:'O meu trabalho',href:'/dashboard#meu-trabalho'}],administrationNavigation:[{label:'Demografia',href:'/management/demography'}],profileAccess:{contexts:[],request:'/perfil/promotor',administration:'/perfis/gestao',audit:'/perfis/auditoria'},scopedAdministration:[{label:'Atribuições do Balcão',href:'/management/balcao/atribuicoes'}]});
|
||||
const admin=groups.find(g=>g.id==='administration');
|
||||
assert.deepEqual(admin.children.map(g=>g.label),['Dashboard','Conteúdos e oportunidades','Cadastros e investimento','Pedidos e acompanhamento','Acessos e configuração']);
|
||||
assert.deepEqual(destinations(admin.children.find(g=>g.label==='Dashboard').children),['/dashboard','/dashboard#meu-trabalho']);
|
||||
for(const href of ['/management/projects','/management/demography','/perfis/gestao','/perfis/auditoria','/management/balcao/atribuicoes']) {
|
||||
assert.equal(destinations(admin.children).filter(h=>h===href).length,1,href);
|
||||
assert.equal(destinations(groups.filter(g=>g.id!=='administration')).includes(href),false,href);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user