fix(navigation): expose work area and preserve authorised menu destinations in QA
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -48,13 +48,15 @@ class HandleInertiaRequests extends Middleware
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$contentManagementAccess=app(\App\Services\ContentManagement::class)->access($request->user());
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'procurementEnabled' => app(\App\Services\Procurement::class)->available(),
|
||||
'homologationReserved' => (bool) config('portal.homologation_only', false),
|
||||
'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()),
|
||||
'contentManagementHref' => app(\App\Services\ContentManagement::class)->access($request->user()) !== [] ? '/gestao-conteudos' : null,
|
||||
'contentManagementHref' => $contentManagementAccess !== [] ? '/gestao-conteudos' : null,
|
||||
'contentManagementAccess'=>$contentManagementAccess,
|
||||
'portalOperator' => config('portal.operator_name'),
|
||||
'portalInstitutionalContactEmail' => filter_var(config('portal.institutional_contact_email'), FILTER_VALIDATE_EMAIL) ?: null,
|
||||
'legacyContentEnabled' => app(LegacyContent::class)->available(),
|
||||
@@ -83,6 +85,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'canManagePortal' => $request->user()?->can('manage-portal') ?? false,
|
||||
'legacyManagementAccess' => $request->user()?->hasVerifiedEmail() && app(PortalProfileAccess::class)->legacyWriteAllowed($request->user()),
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
|
||||
@@ -1702,3 +1702,27 @@ CI habitual do instalador: PostgreSQL1/3106asserções, Unit59/317, frontend1146
|
||||
`6d147693` está instalado, incluindo a base interna. A única pendência da conferência desta entrega é a validação visual autenticada: **Gurué → consultar oferta → comparar ideia → guardar → recuperar no mesmo dossier**, conservando fonte, data e incertezas. As435 observações de Gurué não são435 empresas únicas. Listas internas, cobertura nacional parcial e hipótese de investimento mantêm-se.
|
||||
|
||||
O erro actual, novamente observado, é **“The browser access host is unavailable”**: indisponibilidade do host do conector, distinta de deixar uma aba aberta. A orientação anterior para apenas manter a aba aberta era insuficiente. Restabelecer o conector por mecanismo suportado ou realizar a conferência pela equipa. Não reinstalar nem repetir as provas técnicas aceites; não declarar conferência visual concluída sem o seu resultado. O recibo remoto original e o historial do diagnóstico anterior ficam preservados; esta correcção é documental.
|
||||
|
||||
|
||||
## 28/10/2026 — revisão de menus: inventário antes da verificação do delta
|
||||
|
||||
Origem: recomendação expressa da Tempo após constatar que as entradas indicadas não aparecem ou ficam escondidas. Página relatada:https://teste-investidor.tempo.co.mz/. Preservados6d147693 e a base privada instalada,as provas QA do percurso e da navegação sectorial,os originais,dados,perfis,permissões e históricos. Não se declara uma funcionalidade anterior desaparecida apenas pela ausência de um novo teste.
|
||||
|
||||
Confirmado no código: barra lateral substitui cada grupo pelo seu primeiro destino,omitindo os restantes;menu principal esconde gestão/perfis dentro do menu do nome do utilizador;entrada de gestão de conteúdos existente não é incorporada nessa navegação principal. A conta e os props efectivos da sessão real permanecem por conferir,porque o conector está indisponível;não se presume falta de autorização.
|
||||
|
||||
Incremento em QA: área de trabalho explícita no menu principal,inclusive entrada neutra de acesso;destinos privados apenas segundo capacidades anteriores;mesmo catálogo completo na barra lateral,sem perda dos subdestinos;entrada directa na análise proactiva existente,sem substituir rascunhos editados;preservação de Descoberta/Realização/Suporte e todos os destinos anteriores aplicáveis. Sem alteração activa,schema,regras de negócio ou papéis. Escolhas de apresentação não são exigências organizacionais. Verificação dirigida do delta pendente nesta fotografia;nenhuma nova matriz ou backlog.
|
||||
|
||||
|
||||
## 28/10/2026 — reorganização dos menus implementada em QA, sem instalação
|
||||
|
||||
Recomendação expressa da Tempo após a página principal não apresentar as entradas sugeridas. Causa delimitada no código: a barra lateral omitia subdestinos ao seleccionar apenas o primeiro link de cada grupo; a navegação principal colocava gestão/perfis dentro do menu do nome do utilizador e não incorporava a entrada de gestão já calculada pelo servidor. Não se presume falta de permissões da sessão real, cujos props não foram observados. A entrega6d147693/base interna continua instalada e preservada; esta revisão não reinstala essa base nem repete as provas técnicas aceites.
|
||||
|
||||
**Proposta executável QA:** Área de trabalho visível ao lado de Descoberta,Realização,Suporte; entrada neutra Acesso à gestão explica o estado, mesmo sem acções privadas atribuídas. As acções autorizadas e Preparar análise e ideia vêm das capacidades existentes, sem criar papéis. A análise abre directamente no formulário existente; rascunhos guardados não são substituídos. A barra lateral põe trabalho primeiro e conserva todos os destinos e os filtros territoriais entre catálogos, em vez de reduzir grupos ao primeiro destino. O cabeçalho permite quebra de linha em largura intermédia; os controlos móveis e do teclado existentes são reutilizados.
|
||||
|
||||
**Verificação delimitada:**16 testes de navegação/renderização estrutural,0falhas;tipos,compilação QA e sintaxe PHP passaram. Quatro cenários de capacidades conservaram todos os destinos anteriores; não surgem operações privadas em cenário anónimo ou sem atribuições. SSR usa o componente real da barra lateral com wrappers semânticos de teste; não equivale a captura do navegador. A prévia HTML é um inventário interactivo de destinos, sem pedidos ao portal, identificado como proposta QA, não como sessão real.
|
||||
|
||||
**Pendente:** conferência visual desktop/móvel/teclado/toque e das capacidades efectivas da sessão relatada; o host do conector permanece indisponível no último diagnóstico. Não substituir essa conferência por alegação de acesso demonstrado. A instalação futura deve transportar só o delta de menus pelo processo Git/backup/restauro existente, preservando originais, dados, permissões, contexto,dossier e simulador.435 observações de Gurué não são435empresas únicas; listas internas/cobertura parcial/hipótese preservadas. L8 continua parcial.
|
||||
|
||||
Inventário anterior registado antes dos testes nesta matriz/manual. Actualização apenas nos requisitos existentesV2-01,V2-07,V2-10,MASTER-9dee08d0-0011,MASTER-9f6dd1d9-0001,MASTER-9f6dd1d9-0003,MASTER-02f44baf-0002;não há matriz ou backlog novos. Relatório:`navigation-reconciliation-qa-20261028.json`;prévia:`outputs/navigation-reconciliation-20261028/menu-proposta-qa.html`.
|
||||
|
||||
A apresentação da gestão antiga usa agora a decisão efectiva já existente (`legacyWriteAllowed`, com o requisito de email verificado das rotas), em vez de presumir que o indicador genérico de gestor basta. Não se alteraram regras, atribuições ou permissões. Preservada também a entrada de instalação de perfis anteriormente presente apenas na barra lateral autorizada. Os nomes longos quebram linha na barra lateral, sem reticências que escondam a função.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7260,3 +7260,27 @@ CI habitual do instalador: PostgreSQL1/3106asserções, Unit59/317, frontend1146
|
||||
`6d147693` está instalado, incluindo a base interna. A única pendência da conferência desta entrega é a validação visual autenticada: **Gurué → consultar oferta → comparar ideia → guardar → recuperar no mesmo dossier**, conservando fonte, data e incertezas. As435 observações de Gurué não são435 empresas únicas. Listas internas, cobertura nacional parcial e hipótese de investimento mantêm-se.
|
||||
|
||||
O erro actual, novamente observado, é **“The browser access host is unavailable”**: indisponibilidade do host do conector, distinta de deixar uma aba aberta. A orientação anterior para apenas manter a aba aberta era insuficiente. Restabelecer o conector por mecanismo suportado ou realizar a conferência pela equipa. Não reinstalar nem repetir as provas técnicas aceites; não declarar conferência visual concluída sem o seu resultado. O recibo remoto original e o historial do diagnóstico anterior ficam preservados; esta correcção é documental.
|
||||
|
||||
|
||||
## 28/10/2026 — revisão de menus: inventário antes da verificação do delta
|
||||
|
||||
Origem: recomendação expressa da Tempo após constatar que as entradas indicadas não aparecem ou ficam escondidas. Página relatada:https://teste-investidor.tempo.co.mz/. Preservados6d147693 e a base privada instalada,as provas QA do percurso e da navegação sectorial,os originais,dados,perfis,permissões e históricos. Não se declara uma funcionalidade anterior desaparecida apenas pela ausência de um novo teste.
|
||||
|
||||
Confirmado no código: barra lateral substitui cada grupo pelo seu primeiro destino,omitindo os restantes;menu principal esconde gestão/perfis dentro do menu do nome do utilizador;entrada de gestão de conteúdos existente não é incorporada nessa navegação principal. A conta e os props efectivos da sessão real permanecem por conferir,porque o conector está indisponível;não se presume falta de autorização.
|
||||
|
||||
Incremento em QA: área de trabalho explícita no menu principal,inclusive entrada neutra de acesso;destinos privados apenas segundo capacidades anteriores;mesmo catálogo completo na barra lateral,sem perda dos subdestinos;entrada directa na análise proactiva existente,sem substituir rascunhos editados;preservação de Descoberta/Realização/Suporte e todos os destinos anteriores aplicáveis. Sem alteração activa,schema,regras de negócio ou papéis. Escolhas de apresentação não são exigências organizacionais. Verificação dirigida do delta pendente nesta fotografia;nenhuma nova matriz ou backlog.
|
||||
|
||||
|
||||
## 28/10/2026 — reorganização dos menus implementada em QA, sem instalação
|
||||
|
||||
Recomendação expressa da Tempo após a página principal não apresentar as entradas sugeridas. Causa delimitada no código: a barra lateral omitia subdestinos ao seleccionar apenas o primeiro link de cada grupo; a navegação principal colocava gestão/perfis dentro do menu do nome do utilizador e não incorporava a entrada de gestão já calculada pelo servidor. Não se presume falta de permissões da sessão real, cujos props não foram observados. A entrega6d147693/base interna continua instalada e preservada; esta revisão não reinstala essa base nem repete as provas técnicas aceites.
|
||||
|
||||
**Proposta executável QA:** Área de trabalho visível ao lado de Descoberta,Realização,Suporte; entrada neutra Acesso à gestão explica o estado, mesmo sem acções privadas atribuídas. As acções autorizadas e Preparar análise e ideia vêm das capacidades existentes, sem criar papéis. A análise abre directamente no formulário existente; rascunhos guardados não são substituídos. A barra lateral põe trabalho primeiro e conserva todos os destinos e os filtros territoriais entre catálogos, em vez de reduzir grupos ao primeiro destino. O cabeçalho permite quebra de linha em largura intermédia; os controlos móveis e do teclado existentes são reutilizados.
|
||||
|
||||
**Verificação delimitada:**16 testes de navegação/renderização estrutural,0falhas;tipos,compilação QA e sintaxe PHP passaram. Quatro cenários de capacidades conservaram todos os destinos anteriores; não surgem operações privadas em cenário anónimo ou sem atribuições. SSR usa o componente real da barra lateral com wrappers semânticos de teste; não equivale a captura do navegador. A prévia HTML é um inventário interactivo de destinos, sem pedidos ao portal, identificado como proposta QA, não como sessão real.
|
||||
|
||||
**Pendente:** conferência visual desktop/móvel/teclado/toque e das capacidades efectivas da sessão relatada; o host do conector permanece indisponível no último diagnóstico. Não substituir essa conferência por alegação de acesso demonstrado. A instalação futura deve transportar só o delta de menus pelo processo Git/backup/restauro existente, preservando originais, dados, permissões, contexto,dossier e simulador.435 observações de Gurué não são435empresas únicas; listas internas/cobertura parcial/hipótese preservadas. L8 continua parcial.
|
||||
|
||||
Inventário anterior registado antes dos testes nesta matriz/manual. Actualização apenas nos requisitos existentesV2-01,V2-07,V2-10,MASTER-9dee08d0-0011,MASTER-9f6dd1d9-0001,MASTER-9f6dd1d9-0003,MASTER-02f44baf-0002;não há matriz ou backlog novos. Relatório:`navigation-reconciliation-qa-20261028.json`;prévia:`outputs/navigation-reconciliation-20261028/menu-proposta-qa.html`.
|
||||
|
||||
A apresentação da gestão antiga usa agora a decisão efectiva já existente (`legacyWriteAllowed`, com o requisito de email verificado das rotas), em vez de presumir que o indicador genérico de gestor basta. Não se alteraram regras, atribuições ou permissões. Preservada também a entrada de instalação de perfis anteriormente presente apenas na barra lateral autorizada. Os nomes longos quebram linha na barra lateral, sem reticências que escondam a função.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -236,6 +236,7 @@
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: minmax(71px, auto);
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
||||
column-gap: 0.25rem;
|
||||
padding-inline: 1.25rem;
|
||||
@@ -256,6 +257,7 @@
|
||||
}
|
||||
.tempo-primary-navigation > ul {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { profileLinks } from '@/components/profile-navigation';
|
||||
import {PortalSidebarNavigation} from '@/components/portal-sidebar-navigation';
|
||||
import type { ProfileAccess } from '@/types/profile-access';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { portalNavigation } from '@/lib/portal-navigation';
|
||||
import { portalNavigation,canShowLegacyManagement } from '@/lib/portal-navigation';
|
||||
import AppLogo from '@/components/app-logo';
|
||||
import { NavMain } from '@/components/nav-main';
|
||||
import { NavUser } from '@/components/nav-user';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -20,6 +18,7 @@ import type { Auth } from '@/types';
|
||||
import type { OpportunityManagementAccess } from '@/types/opportunity-management';
|
||||
|
||||
export function AppSidebar() {
|
||||
const currentPage=usePage();
|
||||
const {
|
||||
auth,
|
||||
existingBusinessEnabled,
|
||||
@@ -27,7 +26,9 @@ export function AppSidebar() {
|
||||
aiEnabled,
|
||||
opportunityManagement,
|
||||
contentManagementHref,
|
||||
contentManagementAccess,
|
||||
profileAccess,
|
||||
scopedAdministration,
|
||||
} = usePage<{
|
||||
auth: Auth;
|
||||
existingBusinessEnabled?: boolean;
|
||||
@@ -36,45 +37,26 @@ export function AppSidebar() {
|
||||
profileAccess?: ProfileAccess | null;
|
||||
opportunityManagement?: OpportunityManagementAccess | null;
|
||||
contentManagementHref?: string | null;
|
||||
scopedAdministration?: {label:string;href:string}[];
|
||||
contentManagementAccess?: {opportunities?:{index:string;create?:string|null}};
|
||||
}>().props;
|
||||
const groups = portalNavigation({
|
||||
authenticated: !!auth.user,
|
||||
manager: auth.canManagePortal,
|
||||
manager: canShowLegacyManagement(auth),
|
||||
businesses: existingBusinessEnabled,
|
||||
education: educationEnabled,
|
||||
ai: aiEnabled,
|
||||
opportunityManagement,
|
||||
profileAccess,
|
||||
contentManagementHref,
|
||||
contentManagementAccess,
|
||||
scopedAdministration,
|
||||
legacyContent: currentPage.props.legacyContentEnabled as boolean|undefined,
|
||||
procurement: currentPage.props.procurementEnabled as boolean|undefined,
|
||||
businessReviewUrl: currentPage.props.businessReviewUrl as string|null|undefined,
|
||||
});
|
||||
const items = groups.map((group) => ({
|
||||
title: group.label,
|
||||
href: group.children?.find((item) => item.href)?.href ?? '/',
|
||||
icon: LayoutGrid,
|
||||
}));
|
||||
if (contentManagementHref) {
|
||||
items.unshift({ title: 'Gestão de conteúdos e oportunidades', href: contentManagementHref, icon: LayoutGrid });
|
||||
}
|
||||
if (auth.canManagePortal && opportunityManagement) {
|
||||
items.push({
|
||||
title: opportunityManagement.manage.label,
|
||||
href: opportunityManagement.manage.href,
|
||||
icon: LayoutGrid,
|
||||
});
|
||||
if (opportunityManagement.installed) {
|
||||
items.push({
|
||||
title: opportunityManagement.create.label,
|
||||
href: opportunityManagement.create.href,
|
||||
icon: LayoutGrid,
|
||||
});
|
||||
}
|
||||
}
|
||||
items.push(
|
||||
...profileLinks(profileAccess).map((link) => ({
|
||||
...link,
|
||||
icon: LayoutGrid,
|
||||
})),
|
||||
);
|
||||
return (
|
||||
<Sidebar collapsible="icon" variant="inset">
|
||||
<Sidebar collapsible="offcanvas" variant="inset">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
@@ -88,7 +70,7 @@ export function AppSidebar() {
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<NavMain items={items} />
|
||||
<PortalSidebarNavigation groups={groups} url={currentPage.url}/>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
portalNavigation,
|
||||
canShowLegacyManagement,
|
||||
currentNavigationGroup,
|
||||
currentNavigationItem,
|
||||
} from '@/lib/portal-navigation';
|
||||
@@ -44,6 +45,7 @@ export function PortalGroupedMenu() {
|
||||
auth: {
|
||||
user: { name?: string; email?: string } | null;
|
||||
canManagePortal: boolean;
|
||||
legacyManagementAccess?:boolean;
|
||||
};
|
||||
existingBusinessEnabled?: boolean;
|
||||
businessReviewUrl?: string | null;
|
||||
@@ -54,6 +56,8 @@ export function PortalGroupedMenu() {
|
||||
profileAccess?: ProfileAccess | null;
|
||||
opportunityManagement?: OpportunityManagementAccess | null;
|
||||
scopedAdministration?: { label: string; href: string }[];
|
||||
contentManagementHref?: string | null;
|
||||
contentManagementAccess?: {opportunities?:{index:string;create?:string|null}};
|
||||
}>();
|
||||
const { t, language, requestCount } = useTempoUi();
|
||||
const mobile = useIsMobile();
|
||||
@@ -63,7 +67,7 @@ export function PortalGroupedMenu() {
|
||||
? { id: navigation.active, ...navigation.view.tools }
|
||||
: undefined,
|
||||
authenticated: !!props.auth?.user,
|
||||
manager: !!props.auth?.canManagePortal,
|
||||
manager: canShowLegacyManagement(props.auth),
|
||||
businesses: props.existingBusinessEnabled,
|
||||
businessReviewUrl: props.businessReviewUrl,
|
||||
education: props.educationEnabled,
|
||||
@@ -72,17 +76,10 @@ export function PortalGroupedMenu() {
|
||||
legacyContent: props.legacyContentEnabled,
|
||||
opportunityManagement: props.opportunityManagement,
|
||||
profileAccess: props.profileAccess,
|
||||
contentManagementHref: props.contentManagementHref,
|
||||
contentManagementAccess: props.contentManagementAccess,
|
||||
scopedAdministration: props.scopedAdministration,
|
||||
});
|
||||
if (props.scopedAdministration?.length)
|
||||
groups.push({
|
||||
id: 'scoped-administration',
|
||||
label: 'Administração',
|
||||
children: props.scopedAdministration.map((item, index) => ({
|
||||
id: `scoped-${index}`,
|
||||
label: item.label,
|
||||
href: item.href,
|
||||
})),
|
||||
});
|
||||
const current = currentNavigationGroup(groups, url);
|
||||
const currentItem = currentNavigationItem(groups, url);
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
@@ -134,7 +131,7 @@ export function PortalGroupedMenu() {
|
||||
);
|
||||
const accountGroups = groups.filter(
|
||||
(group) =>
|
||||
!['home', 'discovery', 'realization', 'support'].includes(group.id),
|
||||
!['home', 'discovery', 'realization', 'support','work'].includes(group.id),
|
||||
);
|
||||
const panelLabel = `${t('O meu painel')} — ${requestCount} ${t(requestCount === 1 ? 'pedido recebido' : 'pedidos recebidos')}`;
|
||||
const institutionalCurrent = url.split('?')[0] === '/contacto';
|
||||
@@ -200,6 +197,7 @@ export function PortalGroupedMenu() {
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
'work',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Link} from '@inertiajs/react';
|
||||
import {LayoutGrid} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
import type {PortalNavItem} from '@/lib/portal-navigation';
|
||||
import {currentNavigationItem} from '@/lib/portal-navigation';
|
||||
import {searchTerritoryHref} from '@/lib/search-territory';
|
||||
import {SidebarGroup,SidebarGroupLabel,SidebarMenu,SidebarMenuButton,SidebarMenuItem} from '@/components/ui/sidebar';
|
||||
|
||||
/** Render every authorised destination; a group is never reduced to its first link. */
|
||||
export function PortalSidebarNavigation({groups,url}:{groups:PortalNavItem[];url:string}){
|
||||
const active=currentNavigationItem(groups,url);
|
||||
const ordered=[...groups.filter(group=>group.id==='work'),...groups.filter(group=>group.id!=='work')];
|
||||
const links=(items:PortalNavItem[],depth=0):ReactNode=>items.map(item=>item.children?
|
||||
<li key={item.id} className="list-none"><p className="px-2 pt-3 pb-1 text-xs font-medium text-muted-foreground">{item.label}</p><ul className="ml-2 border-l pl-2">{links(item.children,depth+1)}</ul></li>:
|
||||
item.href?<SidebarMenuItem key={item.id}><SidebarMenuButton asChild isActive={item.id===active} className="h-auto min-h-8 whitespace-normal py-2"><Link href={searchTerritoryHref(item.href,url)} aria-current={item.id===active?'page':undefined}>{depth===0&&<LayoutGrid aria-hidden="true"/>}<span className="min-w-0 flex-1" style={{whiteSpace:'normal',overflow:'visible',textOverflow:'clip',overflowWrap:'anywhere'}}>{item.label}</span></Link></SidebarMenuButton></SidebarMenuItem>:null);
|
||||
return <nav aria-label="Navegação completa do portal">{ordered.map(group=><SidebarGroup key={group.id}><SidebarGroupLabel>{group.label}</SidebarGroupLabel><SidebarMenu>{links(group.children??[])}</SidebarMenu></SidebarGroup>)}</nav>;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ export type PortalNavItem = {
|
||||
children?: PortalNavItem[];
|
||||
};
|
||||
export type PortalNavAccess = {
|
||||
contentManagementHref?: string | null;
|
||||
contentManagementAccess?: {opportunities?:{index:string;create?:string|null}};
|
||||
scopedAdministration?: { label: string; href: string }[];
|
||||
procurement?: boolean;
|
||||
authenticated: boolean;
|
||||
manager: boolean;
|
||||
@@ -31,6 +34,10 @@ const link = (id: string, label: string, href: string): PortalNavItem => ({
|
||||
label,
|
||||
href,
|
||||
});
|
||||
/** Display the existing server decision; a generic manager flag cannot override a scoped denial. */
|
||||
export function canShowLegacyManagement(auth?:{user?:unknown;canManagePortal?:boolean;legacyManagementAccess?:boolean}):boolean {
|
||||
return !!auth?.user && (auth.legacyManagementAccess??auth.canManagePortal??false);
|
||||
}
|
||||
|
||||
/** One route catalogue for the public shell and authenticated pages. */
|
||||
export function legacyPortalNavigation(
|
||||
@@ -371,6 +378,7 @@ export function legacyPortalNavigation(
|
||||
'Gestão de perfis',
|
||||
profile.administration,
|
||||
),
|
||||
link('profile-installation','Instalação dos perfis 040000','/management/profile-install'),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
@@ -439,7 +447,10 @@ export function currentNavigationItem(
|
||||
|
||||
/** Investor organization; existing capability filters still determine private destinations. */
|
||||
export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
const previous = legacyPortalNavigation(access);
|
||||
const previous = legacyPortalNavigation({...access,manager:access.authenticated&&access.manager,profileAccess:access.authenticated?access.profileAccess:null});
|
||||
const workHub=access.authenticated?access.contentManagementHref:null;
|
||||
const workOpportunities=access.authenticated?access.contentManagementAccess?.opportunities:undefined;
|
||||
const workCreate=access.authenticated?(access.profileAccess?.create||workOpportunities?.create||(access.manager&&access.opportunityManagement?.installed?access.opportunityManagement.create.href:null)):null;
|
||||
return uniqueNavigationDestinations([
|
||||
{
|
||||
id: 'discovery',
|
||||
@@ -544,11 +555,20 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
),
|
||||
],
|
||||
},
|
||||
...previous.filter((group) =>
|
||||
['account', 'management', 'profile-opportunities'].includes(
|
||||
group.id,
|
||||
),
|
||||
),
|
||||
...[{
|
||||
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)] : []),
|
||||
...previous.filter(group=>['management','profile-opportunities'].includes(group.id)).flatMap(group=>group.children??[]),
|
||||
...(access.authenticated&&access.scopedAdministration?.length ? [{id:'work-administration',label:'Administração',children:access.scopedAdministration.map((item,index)=>link('scoped-'+index,item.label,item.href))}] : []),
|
||||
],
|
||||
}],
|
||||
...previous.filter((group) => group.id === 'account'),
|
||||
...(access.authenticated &&
|
||||
access.businesses &&
|
||||
access.businessReviewUrl
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ProactiveInvestmentEditor,type ProactiveAnalysis,type ProactiveOptions} from '@/components/proactive-investment';
|
||||
import {ProactiveInvestmentEditor,newProactive,type ProactiveAnalysis,type ProactiveOptions} from '@/components/proactive-investment';
|
||||
import { ClassificationReview } from '@/components/opportunity-classification';
|
||||
import { createOperationKey } from '@/lib/operation-key';
|
||||
import { valueText } from '@/lib/value-text';
|
||||
@@ -140,8 +140,10 @@ export default function OpportunityDraft({
|
||||
auth: { user: { id: number } };
|
||||
canEditDraft?: boolean;
|
||||
}>().props;
|
||||
const proactiveEntryUrl=usePage().url;
|
||||
const [data, setData] = useState<Values>({
|
||||
...values,
|
||||
...(!record && !values.proactive && new URLSearchParams(proactiveEntryUrl.split('?')[1]??'').get('analysis')==='proactive' ? {proactive:newProactive()} : {}),
|
||||
...(profileMode
|
||||
? {
|
||||
organization_id: record
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import fs from 'node:fs';
|
||||
import {createRequire} from 'node:module';
|
||||
import React from 'react';
|
||||
import {renderToStaticMarkup} from 'react-dom/server';
|
||||
import ts from 'typescript';
|
||||
import {portalNavigation,currentNavigationItem} from '../../resources/js/lib/portal-navigation.ts';
|
||||
import {searchTerritoryHref} from '../../resources/js/lib/search-territory.ts';
|
||||
|
||||
// Shallow SSR of the actual navigation component. Inertia and visual primitives
|
||||
// are replaced by semantic wrappers; this is not a browser/viewport proof.
|
||||
const require=createRequire(import.meta.url);
|
||||
const source=fs.readFileSync(new URL('../../resources/js/components/portal-sidebar-navigation.tsx',import.meta.url),'utf8');
|
||||
const output=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText;
|
||||
const primitive=(tag)=>({children,asChild,...props})=>asChild?React.cloneElement(children,{}):React.createElement(tag,props,children);
|
||||
const module={exports:{}};
|
||||
new Function('require','module','exports',output)((name)=>{
|
||||
if(name==='@inertiajs/react')return {Link:({href,children,...props})=>React.createElement('a',{href,...props},children)};
|
||||
if(name==='@/lib/portal-navigation')return {currentNavigationItem};
|
||||
if(name==='@/lib/search-territory')return {searchTerritoryHref};
|
||||
if(name==='lucide-react')return {LayoutGrid:()=>null};
|
||||
if(name==='@/components/ui/sidebar')return {SidebarGroup:primitive('section'),SidebarGroupLabel:primitive('h2'),SidebarMenu:primitive('ul'),SidebarMenuButton:primitive('div'),SidebarMenuItem:primitive('li')};
|
||||
return require(name);
|
||||
},module,module.exports);
|
||||
const Component=module.exports.PortalSidebarNavigation;
|
||||
const flatten=(rows)=>rows.flatMap(r=>[...(r.href?[r.href]:[]),...flatten(r.children??[])]);
|
||||
const render=(access,url='/')=>{const groups=portalNavigation(access);return {groups,html:renderToStaticMarkup(React.createElement(Component,{groups,url}))};};
|
||||
void test('sidebar renders every nested authorised destination instead of a first-child shortcut',()=>{
|
||||
const {groups,html}=render({authenticated:true,manager:true,businesses:true,education:true,ai:true,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}},scopedAdministration:[{label:'Referências económicas',href:'/management/economic-references/lot-01'}]});
|
||||
const hrefs=[...html.matchAll(/<a\b[^>]*href="([^"]+)"/g)].map(m=>m[1].replaceAll('&','&'));
|
||||
assert.deepEqual([...hrefs].sort(),flatten(groups).sort());
|
||||
assert.equal(hrefs[0],'/gestao-conteudos');
|
||||
for(const href of ['/carteira','/zonas','/documentos','/legislacao','/management/opportunities','/management/economic-references/lot-01'])assert.ok(hrefs.includes(href),href);
|
||||
assert.match(html,/Área de trabalho/);
|
||||
});
|
||||
void test('nested current destination is selected without changing its URL or replacing it with a group root',()=>{
|
||||
const {html}=render({authenticated:true,manager:false},'/zonas?category=zee');
|
||||
const current=[...html.matchAll(/<a\b([^>]*aria-current="page"[^>]*)>/g)];
|
||||
assert.equal(current.length,1);
|
||||
assert.match(current[0][1],/href="\/zonas"/);
|
||||
});
|
||||
void test('rendered visitor sidebar explains access without private operation links',()=>{
|
||||
const {html}=render({authenticated:false,manager:false});
|
||||
assert.match(html,/href="\/acesso-gestao"/);
|
||||
assert.doesNotMatch(html,/href="\/management\//);
|
||||
assert.doesNotMatch(html,/analysis=proactive/);
|
||||
});
|
||||
void test('sidebar carries the existing search territory when moving between catalogues',()=>{
|
||||
const {html}=render({authenticated:false,manager:false},'/oportunidades?province_id=QA-province&district_id=QA-district');
|
||||
assert.match(html,/href="\/carteira\?province_id=QA-province&district_id=QA-district"/);
|
||||
assert.match(html,/href="\/zonas\?province_id=QA-province&district_id=QA-district"/);
|
||||
assert.match(html,/href="\/acesso-gestao"/);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
legacyPortalNavigation,
|
||||
currentNavigationGroup,
|
||||
currentNavigationItem,
|
||||
canShowLegacyManagement,
|
||||
} from '../../resources/js/lib/portal-navigation.ts';
|
||||
|
||||
void test('public and authenticated navigation share groups without revealing management', () => {
|
||||
@@ -20,7 +21,7 @@ void test('public and authenticated navigation share groups without revealing ma
|
||||
assert.deepEqual(publicGroups.slice(0, 3), investor.slice(0, 3));
|
||||
assert.deepEqual(
|
||||
publicGroups.map((g) => g.id),
|
||||
['discovery', 'realization', 'support', 'account'],
|
||||
['discovery', 'realization', 'support', 'work', 'account'],
|
||||
);
|
||||
const hrefs = JSON.stringify(investor);
|
||||
assert.equal(hrefs.includes('/management/'), false);
|
||||
@@ -37,7 +38,7 @@ void test('management and optional modules follow permissions and feature flags'
|
||||
education: true,
|
||||
ai: true,
|
||||
});
|
||||
assert.ok(groups.some((group) => group.id === 'management'));
|
||||
assert.ok(groups.some((group) => group.id === 'work'));
|
||||
assert.equal(JSON.stringify(groups).includes('/investidor/negocios'), true);
|
||||
assert.equal(JSON.stringify(groups).includes('/management/ai'), true);
|
||||
assert.equal(
|
||||
@@ -151,3 +152,47 @@ void test('catalogue detail and return preserve the search inside the local cata
|
||||
'/zonas?https%3A%2F%2Foutside.invalid=',
|
||||
);
|
||||
});
|
||||
|
||||
const destinations=(items)=>items.flatMap(item=>[...(item.href?[item.href]:[]),...destinations(item.children??[])]);
|
||||
void test('both menu consumers respect the existing scoped server decision rather than generic manager status',()=>{
|
||||
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true,legacyManagementAccess:false}),false);
|
||||
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true,legacyManagementAccess:true}),true);
|
||||
assert.equal(canShowLegacyManagement({user:null,canManagePortal:true,legacyManagementAccess:true}),false);
|
||||
assert.equal(canShowLegacyManagement({user:{id:1},canManagePortal:true}),true);
|
||||
});
|
||||
void test('work entry is discoverable without exposing private capabilities to an anonymous visitor',()=>{
|
||||
const groups=portalNavigation({authenticated:false,manager:true,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}},scopedAdministration:[{label:'Private QA administration',href:'/management/secret'}]});
|
||||
assert.deepEqual(destinations(groups.find(g=>g.id==='work').children),['/acesso-gestao']);
|
||||
assert.equal(JSON.stringify(groups).includes('/management/'),false);
|
||||
assert.equal(JSON.stringify(groups).includes('/gestao-conteudos'),false);
|
||||
});
|
||||
void test('work area exposes existing content capability even when the old dashboard flag has no menu object',()=>{
|
||||
const groups=portalNavigation({authenticated:true,manager:true,opportunityManagement:null,contentManagementHref:'/gestao-conteudos',contentManagementAccess:{opportunities:{index:'/management/opportunities',create:'/management/opportunities/create'}}});
|
||||
const work=groups.find(g=>g.id==='work');
|
||||
assert.equal(work.label,'Área de trabalho');
|
||||
assert.ok(work.children.some(i=>i.label==='Preparar análise e ideia'&&i.href==='/management/opportunities/create?analysis=proactive'));
|
||||
assert.ok(work.children.some(i=>i.href==='/gestao-conteudos'));
|
||||
assert.ok(work.children.some(i=>i.href==='/management/opportunities'));
|
||||
});
|
||||
void test('every previously authorised management/profile destination survives the regrouping exactly once',()=>{
|
||||
const access={authenticated:true,manager:true,businesses:true,education:true,ai:true,contentManagementHref:'/gestao-conteudos',profileAccess:{contexts:[],mine:'/minhas-oportunidades',create:'/minhas-oportunidades/create',request:'/perfil/promotor',review:null,audit:'/perfis/auditoria',administration:'/perfis/gestao'},scopedAdministration:[{label:'Referências económicas',href:'/management/economic-references/lot-01'}]};
|
||||
const old=destinations(legacyPortalNavigation(access).filter(g=>['management','profile-opportunities'].includes(g.id)));
|
||||
const groups=portalNavigation(access);const after=destinations(groups);
|
||||
for(const href of old)assert.equal(after.filter(x=>x===href).length,1,href);
|
||||
assert.equal(after.length,new Set(after).size);
|
||||
assert.ok(destinations(groups.find(g=>g.id==='work').children).includes('/management/economic-references/lot-01'));
|
||||
assert.equal(after.filter(href=>href==='/management/profile-install').length,1,'profile installation already present in the old authorised sidebar must survive');
|
||||
});
|
||||
void test('ordinary authenticated account has an explanation entry and does not acquire creation or management links',()=>{
|
||||
const groups=portalNavigation({authenticated:true,manager:false});
|
||||
assert.deepEqual(destinations(groups.find(g=>g.id==='work').children),['/acesso-gestao']);
|
||||
assert.equal(JSON.stringify(groups).includes('analysis=proactive'),false);
|
||||
assert.equal(JSON.stringify(groups).includes('/management/'),false);
|
||||
});
|
||||
void test('direct analysis and ordinary creation preserve distinct selection state in the same work group',()=>{
|
||||
const access={authenticated:true,manager:false,profileAccess:{contexts:[],mine:'/minhas-oportunidades',create:'/minhas-oportunidades/create',request:'/perfil/promotor',review:null,administration:null}};
|
||||
const groups=portalNavigation(access);
|
||||
assert.equal(currentNavigationGroup(groups,'/minhas-oportunidades/create?analysis=proactive'),'work');
|
||||
assert.equal(currentNavigationItem(groups,'/minhas-oportunidades/create?analysis=proactive'),'prepare-analysis');
|
||||
assert.equal(currentNavigationItem(groups,'/minhas-oportunidades/create'),'new-my-opportunity');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user