@trismegisto/spine 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,14 +60,20 @@ Tela \`AutoServiceComponent\` (seletor de client + \`AutoServiceAppsComponent\`)
60
60
  - Ids nos caminhos vão codificados (\`urlPath\` de \`@trismegisto/primitives\`).
61
61
  - As abas são preguiçosas (\`*hmTabContent\`): cada uma só busca ao ser aberta. Abrir outra aplicação volta para
62
62
  **Operações**.
63
- - **Gerenciar operações** cruza \`operations\` com \`permissions\` (por \`operation_id\`) e grava só a diferença, em
63
+ - **Gerenciar operações** (mesmo painel do argos-admin) cruza \`operations\` com \`permissions\` (por \`operation_id\`)
64
+ num \`hm-tree\` com caixas: o identifier quebra em ':' (segmentos intermediários viram pastas, com chave terminada
65
+ em ':' para não colidir com uma operação de mesmo nome; a folha é a operação), pastas tri-state com o contador
66
+ \`permissionsFolderCount\` ("X de Y" das escolhíveis), selos de método e nível e a descrição no modelo \`hmTreeNode\`;
67
+ tudo começa recolhido. No topo, \`permissionsSummaryGroup\`/\`permissionsSummaryToken\` e, se houver travadas,
68
+ \`permissionsFreeGroup\`/\`permissionsFreeToken\`. O motivo da trava (\`freeForAll\`, \`freeForAuthenticated\`,
69
+ \`publicNoToken\`...) entra só no \`label\` do nó (nome acessível), não ao lado da operação. Grava só a diferença, em
64
70
  sequência: primeiro as liberações, depois as retiradas. O painel tem \`mode\` (\`PermissionMode\`, em
65
71
  \`operation-level.ts\`):
66
72
  - \`group\` (aba Acessos): só \`protected\` depende de permissão; operação de outro nível sem permissão gravada vem
67
- marcada e desabilitada (\`free\`) e fica fora da diferença (o servidor recusa com 400 \`operation_not_protected\`).
73
+ marcada e desabilitada e fica fora da diferença (o servidor recusa com 400 \`operation_not_protected\`).
68
74
  - \`token\` (aba Tokens): o token não tem login, então \`protected\`, \`private\` (e \`optional\`) são escolhíveis e
69
- nada vem marcado sem permissão gravada; \`public\` vem desmarcada e desabilitada com \`publicNoToken\` (o servidor
70
- recusa com 400 \`operation_public\`).
75
+ nada vem marcado sem permissão gravada; \`public\` vem desmarcada e desabilitada (o servidor recusa com 400
76
+ \`operation_public\`).
71
77
  Nos dois modos, uma permissão antiga numa operação travada segue editável, para dar para retirar.
72
78
  Falha vira toast (\`autoServiceErrorText\`, ou \`permissionsSaveError\`) e o painel fica aberto.
73
79
  - **Tokens** (\`AutoServiceAppTokensComponent\`, \`AutoServiceTokenCreateComponent\`; mirror da aba Tokens do
@@ -86,4 +92,4 @@ Tela \`AutoServiceComponent\` (seletor de client + \`AutoServiceAppsComponent\`)
86
92
  `;
87
93
 
88
94
  export { apps_technical as default };
89
- //# sourceMappingURL=trismegisto-spine-auto-service-apps.technical-dKM4fG42.mjs.map
95
+ //# sourceMappingURL=trismegisto-spine-auto-service-apps.technical-D3W2Vwic.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"trismegisto-spine-auto-service-apps.technical-dKM4fG42.mjs","sources":["../../../projects/ui/auto-service/src/docs/apps.technical.ts"],"sourcesContent":["// Especificação técnica da tela Aplicações do auto-serviço (`RouteDocument.loadTechnical`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Aplicações — especificação técnica\n\nTela \\`AutoServiceComponent\\` (seletor de client + \\`AutoServiceAppsComponent\\`), ligada ao router de template\n\\`AUTO_SERVICE_APPS_TEMPLATE_ROUTER_ID\\` (\\`ad100002-…\\`). Fala com o argos-admin-server em \\`{adminApi}/auto-service\\`\n(\\`AutoServiceService\\`), com o token no cabeçalho \\`x-access-token\\`.\n\n## Endpoints\n\n| Ação | Endpoint |\n| --- | --- |\n| Client administrado (só o do token; ver Grupos) | \\`GET /auto-service\\` |\n| Aplicações do projeto do client | \\`GET /auto-service/:clientId/apps\\` |\n| Cadastrar (devolve o \\`secret\\`) | \\`POST /auto-service/:clientId/apps\\` (\\`{ name, allowed_grants }\\`) |\n| Editar / ativar / desativar | \\`PUT /auto-service/:clientId/apps/:appId\\` (\\`{ name, allowed_grants, enabled }\\` ou só \\`{ enabled }\\`) |\n| Excluir | \\`DELETE /auto-service/:clientId/apps/:appId\\` |\n| Gerar novo segredo | \\`POST /auto-service/:clientId/apps/:appId/secret\\` (resposta \\`{ id, secret }\\`, \\`no-store\\`) |\n| Operações registradas | \\`GET /auto-service/:clientId/apps/:appId/operations\\` |\n| Grupos com acesso (consumers) | \\`GET /auto-service/:clientId/apps/:appId/consumers\\` (\\`{ consumers: [{ consumer_id, group_id, group_name, group_type_name, group_type_level, group_is_admin, app_id, app_name, created_at }] }\\`) |\n| Grupos oferecidos em \"Novo acesso\" | \\`GET /auto-service/:clientId/groups\\` |\n| Novo acesso | \\`POST /auto-service/:clientId/apps/:appId/consumers\\` (\\`{ group_id }\\`) |\n| Remover acesso | \\`DELETE /auto-service/:clientId/apps/consumers/:consumerId\\` |\n| Operações liberadas ao grupo | \\`GET /auto-service/:clientId/apps/consumers/:consumerId/permissions\\` |\n| Liberar / retirar operação | \\`POST .../consumers/:consumerId/permissions\\` (\\`{ operation_id }\\`) / \\`DELETE .../permissions/:permissionId\\` |\n| Tokens de API da aplicação | \\`GET /auto-service/:clientId/apps/:appId/tokens\\` (\\`{ tokens: [{ id, consumer_id, name, description, token_prefix, expires_at, revoked_at, created_at, last_used_at }] }\\`) |\n| Novo token | \\`POST /auto-service/:clientId/apps/:appId/tokens\\` (\\`{ name, description?, expires_at, operation_ids? }\\`; resposta 201 \\`{ id, consumer_id, name, token }\\`) |\n| Revogar token | \\`DELETE /auto-service/:clientId/apps/:appId/tokens/:tokenId\\` (204) |\n| Operações do token (consumer dele) | as mesmas rotas de \\`.../apps/consumers/:consumerId/permissions\\` acima |\n\n## Tela\n\n- \\`AutoServiceAppsComponent\\`: **Cadastrar** e um \\`hm-card\\` com \\`hm-combobox\\` (\\`pickApp\\`, as aplicações de\n \\`GET .../apps\\`) + **Consultar**. A aplicação consultada abre embaixo: resumo com Editar, Desativar/Ativar (o que cabe\n na situação), Gerar novo segredo e Excluir, e as abas Operações, Acessos e Tokens. (Na 3.0.0 a lista era um\n \\`hm-data-grid\\`; voltou ao seletor a pedido do usuário.)\n- Recarregar a lista (salvar, ativar, excluir) mantém a aplicação aberta com os dados novos ou a fecha (e limpa o\n seletor) se sumiu; trocar de client fecha a aplicação aberta e limpa o seletor.\n\n## Regras\n\n- **Autorização** (\\`clientAdminGuard\\`): grupo que administra o projeto do client + crachá Administrador ativo.\n App, consumer ou grupo de outro client responde 404, nunca 403 (\\`assertAppBelongsToClient\\`,\n \\`assertConsumerBelongsToClient\\`, \\`assertBodyGroupVisibleToClient\\`).\n- **Grupo de administração fora da aba Acessos** (decisão 32 do Argos): o servidor não lista o consumer dele, recusa\n \\`POST .../consumers\\` com ele (404 \"Grupo não encontrado\") e responde 404 às permissões e à remoção dele. A tela\n ainda descarta, por defesa, o consumer com \\`group_is_admin: true\\` e o grupo com \\`is_admin: true\\` em \"Novo\n acesso\" (servidor antigo). Consumers de token de API (aba Tokens) não são grupo e não mudam.\n- O \\`secret\\` só vem na resposta do \\`POST\\`; a tela mostra num \\`hm-alert\\` até ser fechado e não o guarda.\n- Grants: texto separado por vírgula, enviado como lista (\\`allowed_grants\\`) sem itens vazios.\n- **Excluir** passa por \\`HermesDialogService.confirm\\` (perigo, com o nome da aplicação) e **Remover** (acesso) pelo\n \\`confirm\\` da ação no \\`hm-data-grid\\`; só depois do sim sai o \\`DELETE\\`. Sucesso e falha viram toast\n (\\`AutoServiceDeleteFeedback\\`: \\`deleteDone\\`, ou o \\`detail\\` do servidor / \\`deleteError\\`). **Ativar/Desativar** chama o\n servidor direto (reversível).\n- O \\`secret\\` exibido some ao trocar de client e ao excluir a aplicação.\n- **Gerar novo segredo** passa por \\`HermesDialogService.confirm\\` (perigo) e mostra o segredo num \\`hm-modal\\` com\n \\`hm-copy-button\\`, uma vez: fechar (Concluir, Esc) descarta. Falha vira toast: 404 (aplicação de outro client),\n 403 (client que a pessoa não administra) ou o \\`detail\\` do servidor / \\`rotateSecretError\\`.\n- Ids nos caminhos vão codificados (\\`urlPath\\` de \\`@trismegisto/primitives\\`).\n- As abas são preguiçosas (\\`*hmTabContent\\`): cada uma só busca ao ser aberta. Abrir outra aplicação volta para\n **Operações**.\n- **Gerenciar operações** cruza \\`operations\\` com \\`permissions\\` (por \\`operation_id\\`) e grava só a diferença, em\n sequência: primeiro as liberações, depois as retiradas. O painel tem \\`mode\\` (\\`PermissionMode\\`, em\n \\`operation-level.ts\\`):\n - \\`group\\` (aba Acessos): só \\`protected\\` depende de permissão; operação de outro nível sem permissão gravada vem\n marcada e desabilitada (\\`free\\`) e fica fora da diferença (o servidor recusa com 400 \\`operation_not_protected\\`).\n - \\`token\\` (aba Tokens): o token não tem login, então \\`protected\\`, \\`private\\` (e \\`optional\\`) são escolhíveis e\n nada vem marcado sem permissão gravada; \\`public\\` vem desmarcada e desabilitada com \\`publicNoToken\\` (o servidor\n recusa com 400 \\`operation_public\\`).\n Nos dois modos, uma permissão antiga numa operação travada segue editável, para dar para retirar.\n Falha vira toast (\\`autoServiceErrorText\\`, ou \\`permissionsSaveError\\`) e o painel fica aberto.\n- **Tokens** (\\`AutoServiceAppTokensComponent\\`, \\`AutoServiceTokenCreateComponent\\`; mirror da aba Tokens do\n argos-admin): situação calculada no cliente (\\`revoked_at\\` → Revogado; \\`expires_at\\` vencido → Expirado; senão\n Ativo). **Novo token** relê as operações da aplicação, com a regra do modo \\`token\\`: nada pré-marcado, \\`protected\\` e\n \\`private\\` escolhíveis, \\`public\\` desmarcada e desabilitada e nunca enviada em \\`operation_ids\\`. Validade num \\`hm-date-picker\\` de amanhã até hoje + 365 dias (padrão 90), enviada\n como a meia-noite LOCAL do dia (\\`localDayStartIso\\`) — o servidor aceita só datas no futuro e até agora + 365 dias.\n O \\`token\\` da resposta aparece uma única vez no modal (\\`hm-copy-button\\`); fechar descarta. Revogar passa pelo\n \\`confirm\\` da grade e vira toast; o token fica na lista como revogado, sem ações. \"Gerenciar operações\" do token abre\n o painel de permissões com o \\`consumer_id\\` do token (o nome no lugar do grupo) e \\`mode=\"token\"\\`.\n- **Erros dos tokens** (\\`autoServiceErrorText\\`, pt-BR/en/es em \\`autoService.tokenErrors\\`): \\`invalid_expires_at\\`,\n \\`invalid_validity\\`, \\`invalid_operation_ids\\`, \\`operation_public\\` (token) e \\`operation_not_protected\\` (grupo); 404 com \\`error\\` falando de aplicação ou de\n token vira \\`appNotFound\\`/\\`tokenNotFound\\`; senão o \\`detail\\` do servidor e, por último, \\`tokenSaveError\\` /\n \\`tokenRevokeError\\` / \\`tokensLoadError\\`. Erro ao criar aparece no modal (que continua aberto).\n- A auditoria das aplicações tem tela própria (Auditoria, \\`ad100004-…\\`).\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,qBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkFd;;"}
1
+ {"version":3,"file":"trismegisto-spine-auto-service-apps.technical-D3W2Vwic.mjs","sources":["../../../projects/ui/auto-service/src/docs/apps.technical.ts"],"sourcesContent":["// Especificação técnica da tela Aplicações do auto-serviço (`RouteDocument.loadTechnical`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Aplicações — especificação técnica\n\nTela \\`AutoServiceComponent\\` (seletor de client + \\`AutoServiceAppsComponent\\`), ligada ao router de template\n\\`AUTO_SERVICE_APPS_TEMPLATE_ROUTER_ID\\` (\\`ad100002-…\\`). Fala com o argos-admin-server em \\`{adminApi}/auto-service\\`\n(\\`AutoServiceService\\`), com o token no cabeçalho \\`x-access-token\\`.\n\n## Endpoints\n\n| Ação | Endpoint |\n| --- | --- |\n| Client administrado (só o do token; ver Grupos) | \\`GET /auto-service\\` |\n| Aplicações do projeto do client | \\`GET /auto-service/:clientId/apps\\` |\n| Cadastrar (devolve o \\`secret\\`) | \\`POST /auto-service/:clientId/apps\\` (\\`{ name, allowed_grants }\\`) |\n| Editar / ativar / desativar | \\`PUT /auto-service/:clientId/apps/:appId\\` (\\`{ name, allowed_grants, enabled }\\` ou só \\`{ enabled }\\`) |\n| Excluir | \\`DELETE /auto-service/:clientId/apps/:appId\\` |\n| Gerar novo segredo | \\`POST /auto-service/:clientId/apps/:appId/secret\\` (resposta \\`{ id, secret }\\`, \\`no-store\\`) |\n| Operações registradas | \\`GET /auto-service/:clientId/apps/:appId/operations\\` |\n| Grupos com acesso (consumers) | \\`GET /auto-service/:clientId/apps/:appId/consumers\\` (\\`{ consumers: [{ consumer_id, group_id, group_name, group_type_name, group_type_level, group_is_admin, app_id, app_name, created_at }] }\\`) |\n| Grupos oferecidos em \"Novo acesso\" | \\`GET /auto-service/:clientId/groups\\` |\n| Novo acesso | \\`POST /auto-service/:clientId/apps/:appId/consumers\\` (\\`{ group_id }\\`) |\n| Remover acesso | \\`DELETE /auto-service/:clientId/apps/consumers/:consumerId\\` |\n| Operações liberadas ao grupo | \\`GET /auto-service/:clientId/apps/consumers/:consumerId/permissions\\` |\n| Liberar / retirar operação | \\`POST .../consumers/:consumerId/permissions\\` (\\`{ operation_id }\\`) / \\`DELETE .../permissions/:permissionId\\` |\n| Tokens de API da aplicação | \\`GET /auto-service/:clientId/apps/:appId/tokens\\` (\\`{ tokens: [{ id, consumer_id, name, description, token_prefix, expires_at, revoked_at, created_at, last_used_at }] }\\`) |\n| Novo token | \\`POST /auto-service/:clientId/apps/:appId/tokens\\` (\\`{ name, description?, expires_at, operation_ids? }\\`; resposta 201 \\`{ id, consumer_id, name, token }\\`) |\n| Revogar token | \\`DELETE /auto-service/:clientId/apps/:appId/tokens/:tokenId\\` (204) |\n| Operações do token (consumer dele) | as mesmas rotas de \\`.../apps/consumers/:consumerId/permissions\\` acima |\n\n## Tela\n\n- \\`AutoServiceAppsComponent\\`: **Cadastrar** e um \\`hm-card\\` com \\`hm-combobox\\` (\\`pickApp\\`, as aplicações de\n \\`GET .../apps\\`) + **Consultar**. A aplicação consultada abre embaixo: resumo com Editar, Desativar/Ativar (o que cabe\n na situação), Gerar novo segredo e Excluir, e as abas Operações, Acessos e Tokens. (Na 3.0.0 a lista era um\n \\`hm-data-grid\\`; voltou ao seletor a pedido do usuário.)\n- Recarregar a lista (salvar, ativar, excluir) mantém a aplicação aberta com os dados novos ou a fecha (e limpa o\n seletor) se sumiu; trocar de client fecha a aplicação aberta e limpa o seletor.\n\n## Regras\n\n- **Autorização** (\\`clientAdminGuard\\`): grupo que administra o projeto do client + crachá Administrador ativo.\n App, consumer ou grupo de outro client responde 404, nunca 403 (\\`assertAppBelongsToClient\\`,\n \\`assertConsumerBelongsToClient\\`, \\`assertBodyGroupVisibleToClient\\`).\n- **Grupo de administração fora da aba Acessos** (decisão 32 do Argos): o servidor não lista o consumer dele, recusa\n \\`POST .../consumers\\` com ele (404 \"Grupo não encontrado\") e responde 404 às permissões e à remoção dele. A tela\n ainda descarta, por defesa, o consumer com \\`group_is_admin: true\\` e o grupo com \\`is_admin: true\\` em \"Novo\n acesso\" (servidor antigo). Consumers de token de API (aba Tokens) não são grupo e não mudam.\n- O \\`secret\\` só vem na resposta do \\`POST\\`; a tela mostra num \\`hm-alert\\` até ser fechado e não o guarda.\n- Grants: texto separado por vírgula, enviado como lista (\\`allowed_grants\\`) sem itens vazios.\n- **Excluir** passa por \\`HermesDialogService.confirm\\` (perigo, com o nome da aplicação) e **Remover** (acesso) pelo\n \\`confirm\\` da ação no \\`hm-data-grid\\`; só depois do sim sai o \\`DELETE\\`. Sucesso e falha viram toast\n (\\`AutoServiceDeleteFeedback\\`: \\`deleteDone\\`, ou o \\`detail\\` do servidor / \\`deleteError\\`). **Ativar/Desativar** chama o\n servidor direto (reversível).\n- O \\`secret\\` exibido some ao trocar de client e ao excluir a aplicação.\n- **Gerar novo segredo** passa por \\`HermesDialogService.confirm\\` (perigo) e mostra o segredo num \\`hm-modal\\` com\n \\`hm-copy-button\\`, uma vez: fechar (Concluir, Esc) descarta. Falha vira toast: 404 (aplicação de outro client),\n 403 (client que a pessoa não administra) ou o \\`detail\\` do servidor / \\`rotateSecretError\\`.\n- Ids nos caminhos vão codificados (\\`urlPath\\` de \\`@trismegisto/primitives\\`).\n- As abas são preguiçosas (\\`*hmTabContent\\`): cada uma só busca ao ser aberta. Abrir outra aplicação volta para\n **Operações**.\n- **Gerenciar operações** (mesmo painel do argos-admin) cruza \\`operations\\` com \\`permissions\\` (por \\`operation_id\\`)\n num \\`hm-tree\\` com caixas: o identifier quebra em ':' (segmentos intermediários viram pastas, com chave terminada\n em ':' para não colidir com uma operação de mesmo nome; a folha é a operação), pastas tri-state com o contador\n \\`permissionsFolderCount\\` (\"X de Y\" das escolhíveis), selos de método e nível e a descrição no modelo \\`hmTreeNode\\`;\n tudo começa recolhido. No topo, \\`permissionsSummaryGroup\\`/\\`permissionsSummaryToken\\` e, se houver travadas,\n \\`permissionsFreeGroup\\`/\\`permissionsFreeToken\\`. O motivo da trava (\\`freeForAll\\`, \\`freeForAuthenticated\\`,\n \\`publicNoToken\\`...) entra só no \\`label\\` do nó (nome acessível), não ao lado da operação. Grava só a diferença, em\n sequência: primeiro as liberações, depois as retiradas. O painel tem \\`mode\\` (\\`PermissionMode\\`, em\n \\`operation-level.ts\\`):\n - \\`group\\` (aba Acessos): só \\`protected\\` depende de permissão; operação de outro nível sem permissão gravada vem\n marcada e desabilitada e fica fora da diferença (o servidor recusa com 400 \\`operation_not_protected\\`).\n - \\`token\\` (aba Tokens): o token não tem login, então \\`protected\\`, \\`private\\` (e \\`optional\\`) são escolhíveis e\n nada vem marcado sem permissão gravada; \\`public\\` vem desmarcada e desabilitada (o servidor recusa com 400\n \\`operation_public\\`).\n Nos dois modos, uma permissão antiga numa operação travada segue editável, para dar para retirar.\n Falha vira toast (\\`autoServiceErrorText\\`, ou \\`permissionsSaveError\\`) e o painel fica aberto.\n- **Tokens** (\\`AutoServiceAppTokensComponent\\`, \\`AutoServiceTokenCreateComponent\\`; mirror da aba Tokens do\n argos-admin): situação calculada no cliente (\\`revoked_at\\` → Revogado; \\`expires_at\\` vencido → Expirado; senão\n Ativo). **Novo token** relê as operações da aplicação, com a regra do modo \\`token\\`: nada pré-marcado, \\`protected\\` e\n \\`private\\` escolhíveis, \\`public\\` desmarcada e desabilitada e nunca enviada em \\`operation_ids\\`. Validade num \\`hm-date-picker\\` de amanhã até hoje + 365 dias (padrão 90), enviada\n como a meia-noite LOCAL do dia (\\`localDayStartIso\\`) — o servidor aceita só datas no futuro e até agora + 365 dias.\n O \\`token\\` da resposta aparece uma única vez no modal (\\`hm-copy-button\\`); fechar descarta. Revogar passa pelo\n \\`confirm\\` da grade e vira toast; o token fica na lista como revogado, sem ações. \"Gerenciar operações\" do token abre\n o painel de permissões com o \\`consumer_id\\` do token (o nome no lugar do grupo) e \\`mode=\"token\"\\`.\n- **Erros dos tokens** (\\`autoServiceErrorText\\`, pt-BR/en/es em \\`autoService.tokenErrors\\`): \\`invalid_expires_at\\`,\n \\`invalid_validity\\`, \\`invalid_operation_ids\\`, \\`operation_public\\` (token) e \\`operation_not_protected\\` (grupo); 404 com \\`error\\` falando de aplicação ou de\n token vira \\`appNotFound\\`/\\`tokenNotFound\\`; senão o \\`detail\\` do servidor e, por último, \\`tokenSaveError\\` /\n \\`tokenRevokeError\\` / \\`tokensLoadError\\`. Erro ao criar aparece no modal (que continua aberto).\n- A auditoria das aplicações tem tela própria (Auditoria, \\`ad100004-…\\`).\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,qBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwFd;;"}
@@ -51,9 +51,11 @@ o acesso dele às aplicações é gerido só no painel central do Argos.
51
51
  - **Novo acesso**: escolha o **Grupo** e clique em **Criar**. Só aparecem os grupos do seu client (sem o de
52
52
  administração).
53
53
  - No menu de cada linha:
54
- - **Gerenciar operações** abre o painel **Operações de nome do grupo**, com uma caixa por operação
55
- (identificador, método, nível e descrição). Só as operações **protected** dependem de permissão: as **public** e
56
- **private** aparecem marcadas e travadas (liberadas para todos ou para autenticados). Marque o que o grupo pode
54
+ - **Gerenciar operações** abre o painel **Operações de nome do grupo**: uma árvore com as operações agrupadas pelo
55
+ identificador (\`teams:members:add\` fica na pasta **teams** › **members**), cada uma com método, nível e descrição.
56
+ No topo, um resumo diz quantas operações protegidas estão liberadas para o grupo. Só as **protected** dependem de
57
+ permissão: as **public**, **private** e **optional** aparecem marcadas e travadas. Cada pasta mostra quantas das
58
+ operações escolhíveis dela estão marcadas (**X de Y**) e marcá-la marca todas elas. Marque o que o grupo pode
57
59
  chamar, desmarque o que não pode e clique em **Confirmar alterações** (o botão só habilita quando algo mudou).
58
60
  **Cancelar** descarta.
59
61
  - **Remover** pede confirmação (**Remover acesso à aplicação?**, com o nome do grupo) e só então tira o acesso do
@@ -77,9 +79,9 @@ da aplicação sem login. A tabela mostra **Nome**, **Início do token** (os pri
77
79
  2. **Criar token** mostra o valor do token **uma única vez**, com o botão **Copiar**. **Copie e guarde na hora:
78
80
  fechando a janela (Concluir ou Esc) não há como vê-lo de novo.** Se ele se perder, revogue e crie outro.
79
81
  3. No menu de cada linha:
80
- - **Gerenciar operações** abre o painel de operações do token: marca o que já está liberado, deixa marcar e
81
- desmarcar as **protected** e **private** e mantém as **public** travadas e desmarcadas. **Confirmar
82
- alterações** grava só o que mudou.
82
+ - **Gerenciar operações** abre o mesmo painel em árvore, para o token: marca o que já está liberado, deixa marcar e
83
+ desmarcar as **protected**, **private** e **optional** e mantém as **public** travadas e desmarcadas (o resumo
84
+ do topo diz quantas são). **Confirmar alterações** grava só o que mudou.
83
85
  - **Revogar** pede confirmação (**Revogar token?**, com o nome dele). Confirmando, o token deixa de funcionar na
84
86
  hora e fica na lista como **Revogado**. Não tem volta.
85
87
 
@@ -88,4 +90,4 @@ motivo aparece no próprio formulário.
88
90
  `;
89
91
 
90
92
  export { apps_usage as default };
91
- //# sourceMappingURL=trismegisto-spine-auto-service-apps.usage-VMrCG5o3.mjs.map
93
+ //# sourceMappingURL=trismegisto-spine-auto-service-apps.usage-DeLfYMeO.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"trismegisto-spine-auto-service-apps.usage-VMrCG5o3.mjs","sources":["../../../projects/ui/auto-service/src/docs/apps.usage.ts"],"sourcesContent":["// Manual de uso da tela Aplicações do auto-serviço (`RouteDocument.load`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Aplicações\n\nGerencie as aplicações (credenciais \\`client_credentials\\`) do projeto do seu client, sem depender do painel\ncentral: cadastre, ative ou desative e exclua aplicações, veja as operações que cada uma registra, defina quais\ngrupos podem chamar quais operações e crie os tokens de API das aplicações.\n\n## Antes de começar\n\nComo nas outras telas do auto-serviço, o client administrado é o do app em que você está logado: não há client\npara escolher. É preciso estar no grupo de administração do projeto desse app e com o crachá **Administrador**\nativo.\n\n## Como usar\n\n1. A tela abre com o botão **Cadastrar** e um cartão com o seletor **Selecione uma aplicação**, que lista as\n aplicações do projeto. Escolha uma e clique em **Consultar**.\n2. Para criar uma aplicação, clique em **Cadastrar**. No modal **Nova aplicação**, preencha o\n **Nome** (obrigatório) e os **Grants** (separados por vírgula; já vem \\`client_credentials\\`) e clique em **Salvar**.\n3. Ao criar, aparece no topo da tela o aviso **Aplicação criada** com o secret. **Copie e guarde o secret na hora:\n ele não é exibido de novo.** O aviso some quando você o fecha, troca de client ou exclui a aplicação.\n4. A aplicação consultada abre embaixo do seletor: um cartão com o nome, a situação (**Ativo** ou **Inativo**),\n os grants e os botões das ações da aplicação:\n - **Editar**: muda nome, grants e a caixa **Ativo**.\n - **Desativar** / **Ativar**: liga ou desliga a aplicação sem apagar nada.\n - **Gerar novo segredo**: abre a confirmação **Gerar um novo segredo?**. Confirmando, o segredo atual deixa de\n funcionar na hora (atualize o APP_SECRET do serviço que usa a aplicação; tokens já emitidos valem até expirar,\n até 1 hora) e o novo aparece numa janela, **uma única vez**, com o botão **Copiar**. **Concluir** fecha a janela.\n - **Excluir**: abre a confirmação **Excluir aplicação?** com o nome dela; só depois de **Excluir** no diálogo a\n aplicação é apagada e a credencial dela para de funcionar. **Cancelar** (ou Esc) não muda nada.\n5. Embaixo do cartão ficam as abas **Operações**, **Acessos** e **Tokens** da aplicação. Consultar outra\n aplicação volta para a aba **Operações**.\n\n> **Desativar** age na hora e se desfaz com **Ativar**. **Excluir** não tem volta: na dúvida, desative. Depois de\n> excluir aparece a mensagem **Exclusão concluída.**; se o servidor recusar, a mensagem de erro diz o motivo e a\n> aplicação continua na tela.\n\n## Aba Operações\n\nLista, só para consulta, as operações registradas pela aplicação: **Identificador**, **Método**, **Nível** (o selo\nfica verde para operação pública e amarelo para protegida), **Descrição** e **Grupos permitidos** (quantos grupos já\npodem chamar a operação). A tela não cria nem edita operações.\n\n## Aba Acessos\n\nMostra os grupos que têm acesso à aplicação (**Grupo** e **Tipo**). O grupo de administração do projeto não aparece:\no acesso dele às aplicações é gerido só no painel central do Argos.\n\n- **Novo acesso**: escolha o **Grupo** e clique em **Criar**. Só aparecem os grupos do seu client (sem o de\n administração).\n- No menu de cada linha:\n - **Gerenciar operações** abre o painel **Operações de nome do grupo**, com uma caixa por operação\n (identificador, método, nível e descrição). Só as operações **protected** dependem de permissão: as **public** e\n **private** aparecem marcadas e travadas (liberadas para todos ou para autenticados). Marque o que o grupo pode\n chamar, desmarque o que não pode e clique em **Confirmar alterações** (o botão só habilita quando algo mudou).\n **Cancelar** descarta.\n - **Remover** pede confirmação (**Remover acesso à aplicação?**, com o nome do grupo) e só então tira o acesso do\n grupo à aplicação e as permissões de operação dele.\n\nDepois de criar o acesso, use **Gerenciar operações** para escolher o que o grupo pode chamar.\n\n## Aba Tokens\n\nTokens de API da aplicação: a credencial que um serviço envia no cabeçalho \\`x-api-token\\` para chamar as operações\nda aplicação sem login. A tabela mostra **Nome**, **Início do token** (os primeiros caracteres, para reconhecer),\n**Situação** (**Ativo**, **Expirado** ou **Revogado**), **Expira em** e **Último uso**.\n\n1. **Novo token** abre o formulário:\n - **Nome** (obrigatório) e **Descrição** (opcional).\n - **Expira em**: de amanhã até 365 dias a partir de hoje (vem com 90 dias). O token vale até a meia-noite desse\n dia.\n - **Operações liberadas ao token**: nada vem marcado. O token não tem login, então marque as operações\n **protected**, **private** e **optional** que ele pode chamar; o que ficar desmarcado ele não chama. As **public** aparecem\n desmarcadas e travadas, com a nota **pública: não precisa de token**.\n2. **Criar token** mostra o valor do token **uma única vez**, com o botão **Copiar**. **Copie e guarde na hora:\n fechando a janela (Concluir ou Esc) não há como vê-lo de novo.** Se ele se perder, revogue e crie outro.\n3. No menu de cada linha:\n - **Gerenciar operações** abre o painel de operações do token: marca o que já está liberado, deixa marcar e\n desmarcar as **protected** e **private** e mantém as **public** travadas e desmarcadas. **Confirmar\n alterações** grava só o que mudou.\n - **Revogar** pede confirmação (**Revogar token?**, com o nome dele). Confirmando, o token deixa de funcionar na\n hora e fica na lista como **Revogado**. Não tem volta.\n\nToken revogado não tem mais ações. Se o servidor recusar a criação (por exemplo, validade fora do intervalo), o\nmotivo aparece no próprio formulário.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,iBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoFd;;"}
1
+ {"version":3,"file":"trismegisto-spine-auto-service-apps.usage-DeLfYMeO.mjs","sources":["../../../projects/ui/auto-service/src/docs/apps.usage.ts"],"sourcesContent":["// Manual de uso da tela Aplicações do auto-serviço (`RouteDocument.load`), em Markdown. Módulo próprio,\n// carregado só pelo `import()` de `AUTO_SERVICE_DOCUMENTS`: vira um chunk à parte no FESM e fica fora do\n// bundle inicial. Não importe estaticamente nem reexporte no `public-api.ts`.\nexport default `# Aplicações\n\nGerencie as aplicações (credenciais \\`client_credentials\\`) do projeto do seu client, sem depender do painel\ncentral: cadastre, ative ou desative e exclua aplicações, veja as operações que cada uma registra, defina quais\ngrupos podem chamar quais operações e crie os tokens de API das aplicações.\n\n## Antes de começar\n\nComo nas outras telas do auto-serviço, o client administrado é o do app em que você está logado: não há client\npara escolher. É preciso estar no grupo de administração do projeto desse app e com o crachá **Administrador**\nativo.\n\n## Como usar\n\n1. A tela abre com o botão **Cadastrar** e um cartão com o seletor **Selecione uma aplicação**, que lista as\n aplicações do projeto. Escolha uma e clique em **Consultar**.\n2. Para criar uma aplicação, clique em **Cadastrar**. No modal **Nova aplicação**, preencha o\n **Nome** (obrigatório) e os **Grants** (separados por vírgula; já vem \\`client_credentials\\`) e clique em **Salvar**.\n3. Ao criar, aparece no topo da tela o aviso **Aplicação criada** com o secret. **Copie e guarde o secret na hora:\n ele não é exibido de novo.** O aviso some quando você o fecha, troca de client ou exclui a aplicação.\n4. A aplicação consultada abre embaixo do seletor: um cartão com o nome, a situação (**Ativo** ou **Inativo**),\n os grants e os botões das ações da aplicação:\n - **Editar**: muda nome, grants e a caixa **Ativo**.\n - **Desativar** / **Ativar**: liga ou desliga a aplicação sem apagar nada.\n - **Gerar novo segredo**: abre a confirmação **Gerar um novo segredo?**. Confirmando, o segredo atual deixa de\n funcionar na hora (atualize o APP_SECRET do serviço que usa a aplicação; tokens já emitidos valem até expirar,\n até 1 hora) e o novo aparece numa janela, **uma única vez**, com o botão **Copiar**. **Concluir** fecha a janela.\n - **Excluir**: abre a confirmação **Excluir aplicação?** com o nome dela; só depois de **Excluir** no diálogo a\n aplicação é apagada e a credencial dela para de funcionar. **Cancelar** (ou Esc) não muda nada.\n5. Embaixo do cartão ficam as abas **Operações**, **Acessos** e **Tokens** da aplicação. Consultar outra\n aplicação volta para a aba **Operações**.\n\n> **Desativar** age na hora e se desfaz com **Ativar**. **Excluir** não tem volta: na dúvida, desative. Depois de\n> excluir aparece a mensagem **Exclusão concluída.**; se o servidor recusar, a mensagem de erro diz o motivo e a\n> aplicação continua na tela.\n\n## Aba Operações\n\nLista, só para consulta, as operações registradas pela aplicação: **Identificador**, **Método**, **Nível** (o selo\nfica verde para operação pública e amarelo para protegida), **Descrição** e **Grupos permitidos** (quantos grupos já\npodem chamar a operação). A tela não cria nem edita operações.\n\n## Aba Acessos\n\nMostra os grupos que têm acesso à aplicação (**Grupo** e **Tipo**). O grupo de administração do projeto não aparece:\no acesso dele às aplicações é gerido só no painel central do Argos.\n\n- **Novo acesso**: escolha o **Grupo** e clique em **Criar**. Só aparecem os grupos do seu client (sem o de\n administração).\n- No menu de cada linha:\n - **Gerenciar operações** abre o painel **Operações de nome do grupo**: uma árvore com as operações agrupadas pelo\n identificador (\\`teams:members:add\\` fica na pasta **teams** › **members**), cada uma com método, nível e descrição.\n No topo, um resumo diz quantas operações protegidas estão liberadas para o grupo. Só as **protected** dependem de\n permissão: as **public**, **private** e **optional** aparecem marcadas e travadas. Cada pasta mostra quantas das\n operações escolhíveis dela estão marcadas (**X de Y**) e marcá-la marca todas elas. Marque o que o grupo pode\n chamar, desmarque o que não pode e clique em **Confirmar alterações** (o botão só habilita quando algo mudou).\n **Cancelar** descarta.\n - **Remover** pede confirmação (**Remover acesso à aplicação?**, com o nome do grupo) e só então tira o acesso do\n grupo à aplicação e as permissões de operação dele.\n\nDepois de criar o acesso, use **Gerenciar operações** para escolher o que o grupo pode chamar.\n\n## Aba Tokens\n\nTokens de API da aplicação: a credencial que um serviço envia no cabeçalho \\`x-api-token\\` para chamar as operações\nda aplicação sem login. A tabela mostra **Nome**, **Início do token** (os primeiros caracteres, para reconhecer),\n**Situação** (**Ativo**, **Expirado** ou **Revogado**), **Expira em** e **Último uso**.\n\n1. **Novo token** abre o formulário:\n - **Nome** (obrigatório) e **Descrição** (opcional).\n - **Expira em**: de amanhã até 365 dias a partir de hoje (vem com 90 dias). O token vale até a meia-noite desse\n dia.\n - **Operações liberadas ao token**: nada vem marcado. O token não tem login, então marque as operações\n **protected**, **private** e **optional** que ele pode chamar; o que ficar desmarcado ele não chama. As **public** aparecem\n desmarcadas e travadas, com a nota **pública: não precisa de token**.\n2. **Criar token** mostra o valor do token **uma única vez**, com o botão **Copiar**. **Copie e guarde na hora:\n fechando a janela (Concluir ou Esc) não há como vê-lo de novo.** Se ele se perder, revogue e crie outro.\n3. No menu de cada linha:\n - **Gerenciar operações** abre o mesmo painel em árvore, para o token: marca o que já está liberado, deixa marcar e\n desmarcar as **protected**, **private** e **optional** e mantém as **public** travadas e desmarcadas (o resumo\n do topo diz quantas são). **Confirmar alterações** grava só o que mudou.\n - **Revogar** pede confirmação (**Revogar token?**, com o nome dele). Confirmando, o token deixa de funcionar na\n hora e fica na lista como **Revogado**. Não tem volta.\n\nToken revogado não tem mais ações. Se o servidor recusar a criação (por exemplo, validade fora do intervalo), o\nmotivo aparece no próprio formulário.\n`;\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA,iBAAe,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsFd;;"}
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ChangeDetectorRef, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, computed } from '@angular/core';
2
+ import { inject, ChangeDetectorRef, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, signal, computed } from '@angular/core';
3
3
  import * as i2 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import { A as AutoServiceClientPickerComponent } from './trismegisto-spine-auto-service-auto-service-client-picker.component-DdtDk_xl.mjs';
6
6
  import { HttpErrorResponse } from '@angular/common/http';
7
- import { ButtonComponent, CheckboxComponent, InputComponent, ModalComponent, ComboboxComponent, HermesToastService, DrawerComponent, IconComponent, TagComponent, DataGridComponent, DataGridCellDirective, AlertComponent, CopyButtonComponent, DatePickerComponent, TextareaComponent, HermesDialogService, CardComponent, TabsComponent, TabComponent, TabContentDirective } from '@trismegisto/angular';
7
+ import { ButtonComponent, CheckboxComponent, InputComponent, ModalComponent, ComboboxComponent, HermesToastService, DrawerComponent, IconComponent, TagComponent, TreeComponent, TreeNodeTemplateDirective, DataGridComponent, DataGridCellDirective, AlertComponent, CopyButtonComponent, DatePickerComponent, TextareaComponent, HermesDialogService, CardComponent, TabsComponent, TabComponent, TabContentDirective } from '@trismegisto/angular';
8
8
  import { formatMessage, pluralMessage } from '@trismegisto/primitives';
9
9
  import { SPINE_MESSAGES } from '@trismegisto/spine';
10
10
  import { a as autoServiceErrorText, A as AutoServiceDeleteFeedback } from './trismegisto-spine-auto-service-auto-service-delete-B1tXgMvi.mjs';
@@ -175,28 +175,107 @@ function levelTagColor(level) {
175
175
  }
176
176
 
177
177
  /**
178
- * "Gerenciar operações" de um consumer da aplicação: a caixa de cada operação e a diferença gravada ao confirmar.
179
- * `mode` diz de quem é o consumer: `group` (aba Acessos; public/private já valem e vêm marcadas e travadas) ou `token`
180
- * (aba Tokens; nada vem marcado, protected e private são escolhíveis e public fica travada e desmarcada).
178
+ * "Gerenciar operações" de um consumer da aplicação: árvore (`hm-tree`) das operações agrupadas pelo identificador, e a
179
+ * diferença gravada ao confirmar. `mode` diz de quem é o consumer: `group` (aba Acessos; public/private/optional já valem
180
+ * e vêm marcadas e travadas) ou `token` (aba Tokens; nada vem marcado, private/optional/protected são escolhíveis e
181
+ * public fica travada e desmarcada).
181
182
  */
182
183
  class AutoServiceConsumerPermissionsComponent {
184
+ set mode(value) { this.modeSig.set(value); }
185
+ get mode() { return this.modeSig(); }
183
186
  constructor(service) {
184
187
  this.service = service;
185
- this.text = inject(SPINE_MESSAGES).autoService;
188
+ this.messages = inject(SPINE_MESSAGES);
189
+ this.text = this.messages.autoService;
186
190
  /** Telas com estado em campo comum, preenchido depois de `await`: marca a view ao terminar (app sem zone.js). */
187
191
  this.cdr = inject(ChangeDetectorRef);
188
192
  this.fmt = formatMessage;
189
- this.levelTagColor = levelTagColor;
190
193
  this.toast = inject(HermesToastService);
191
194
  this.clientId = '';
192
195
  this.appId = '';
193
196
  this.consumer = null;
194
- this.mode = 'group';
195
197
  this.open = false;
196
198
  this.openChange = new EventEmitter();
197
- this.rows = [];
198
199
  this.loading = false;
199
200
  this.saving = false;
201
+ this.modeSig = signal('group', ...(ngDevMode ? [{ debugName: "modeSig" }] : /* istanbul ignore next */ []));
202
+ this.tree = signal([], ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
203
+ /** Só operações escolhíveis (não travadas): é o que o consumer pode ganhar ou perder. */
204
+ this.selectedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
205
+ this.expandedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedIds" }] : /* istanbul ignore next */ []));
206
+ this.originalSelectedIds = new Set();
207
+ /** Todos os nós (pastas e folhas) por chave. */
208
+ this.nodesByKey = computed(() => {
209
+ const map = new Map();
210
+ const walk = (nodes) => { for (const n of nodes) {
211
+ map.set(n.key, n);
212
+ walk(n.children);
213
+ } };
214
+ walk(this.tree());
215
+ return map;
216
+ }, ...(ngDevMode ? [{ debugName: "nodesByKey" }] : /* istanbul ignore next */ []));
217
+ this.leaves = computed(() => this.tree().flatMap(n => collectLeaves(n)), ...(ngDevMode ? [{ debugName: "leaves" }] : /* istanbul ignore next */ []));
218
+ this.lockedIds = computed(() => new Set(this.leaves().filter(l => l.locked).map(l => l.key)), ...(ngDevMode ? [{ debugName: "lockedIds" }] : /* istanbul ignore next */ []));
219
+ /** Marcação mostrada na árvore: as escolhidas e, no GRUPO, as travadas (já valem para ele). No token as travadas
220
+ * (public) aparecem desmarcadas. */
221
+ this.treeSelected = computed(() => this.modeSig() === 'token' ? this.selectedIds() : new Set([...this.selectedIds(), ...this.lockedIds()]), ...(ngDevMode ? [{ debugName: "treeSelected" }] : /* istanbul ignore next */ []));
222
+ /** Linha de resumo no topo: quantas escolhíveis estão liberadas e quantas não dependem de permissão. */
223
+ this.summary = computed(() => {
224
+ const total = this.leaves().filter(l => !l.locked).length;
225
+ const marked = this.selectedIds().size;
226
+ return formatMessage(this.modeSig() === 'token' ? this.text.permissionsSummaryToken : this.text.permissionsSummaryGroup, { marked, total });
227
+ }, ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
228
+ this.freeSummary = computed(() => {
229
+ const n = this.lockedIds().size;
230
+ if (!n)
231
+ return '';
232
+ return pluralMessage(this.modeSig() === 'token' ? this.text.permissionsFreeToken : this.text.permissionsFreeGroup, n, this.messages.meta.locale);
233
+ }, ...(ngDevMode ? [{ debugName: "freeSummary" }] : /* istanbul ignore next */ []));
234
+ /** hm-tree é lista achatada com estado só em `selected`/`expanded`. O label leva o texto completo (nome, método,
235
+ * nível, descrição e por que está travada, para leitor de tela); o que aparece vem do modelo `hmTreeNode`. */
236
+ this.treeNodes = computed(() => this.tree().map(n => this.toTreeNode(n)), ...(ngDevMode ? [{ debugName: "treeNodes" }] : /* istanbul ignore next */ []));
237
+ this.nodeInfo = computed(() => {
238
+ const selected = this.selectedIds();
239
+ const map = new Map();
240
+ const walk = (nodes) => {
241
+ for (const n of nodes) {
242
+ if (n.isFolder) {
243
+ const open = collectLeaves(n).filter(l => !l.locked);
244
+ const marked = open.filter(l => selected.has(l.key)).length;
245
+ map.set(n.key, {
246
+ name: n.name, isFolder: true,
247
+ count: open.length ? formatMessage(this.text.permissionsFolderCount, { marked, total: open.length }) : '',
248
+ });
249
+ walk(n.children);
250
+ }
251
+ else {
252
+ map.set(n.key, {
253
+ name: n.name, isFolder: false, method: n.method, level: n.level, levelColor: levelTagColor(n.level),
254
+ description: n.description,
255
+ });
256
+ }
257
+ }
258
+ };
259
+ walk(this.tree());
260
+ return map;
261
+ }, ...(ngDevMode ? [{ debugName: "nodeInfo" }] : /* istanbul ignore next */ []));
262
+ }
263
+ info(node) {
264
+ return this.nodeInfo().get(node.id) ?? { name: node.label, isFolder: !node.leaf };
265
+ }
266
+ toTreeNode(node) {
267
+ if (node.isFolder) {
268
+ const leaves = collectLeaves(node);
269
+ return {
270
+ id: node.key, label: node.name, leaf: false,
271
+ // Pasta só com operações travadas: nada a marcar dentro dela.
272
+ disabled: leaves.length > 0 && leaves.every(l => l.locked),
273
+ children: node.children.map(c => this.toTreeNode(c)),
274
+ };
275
+ }
276
+ const free = node.locked ? freeAccessText(node.level, this.text, this.modeSig()) : '';
277
+ const label = `${node.name} — ${node.method} · ${node.level}${node.description ? ' · ' + node.description : ''}${free ? ' · ' + free : ''}`;
278
+ return { id: node.key, label, leaf: true, disabled: !!node.locked };
200
279
  }
201
280
  ngOnChanges(changes) { if (changes['open'] && this.open) {
202
281
  this.reset();
@@ -211,40 +290,95 @@ class AutoServiceConsumerPermissionsComponent {
211
290
  ]);
212
291
  const operations = opsRes.operations ?? [];
213
292
  const permissions = permsRes.permissions ?? [];
214
- this.rows = operations.map((op) => {
293
+ const root = [];
294
+ const sorted = [...operations].sort((a, b) => String(a.identifier).localeCompare(String(b.identifier)));
295
+ for (const op of sorted) {
215
296
  const perm = permissions.find((p) => p.operation_id === op.operation_id);
216
- // Operação que não depende de permissão neste modo (o servidor recusa liberá-la): sem permissão gravada, a caixa
217
- // vem travada (marcada no grupo, desmarcada no token); com uma antiga, fica editável para dar para retirar.
218
- const free = !perm && !requiresPermission(op.level, this.mode) ? freeAccessText(op.level, this.text, this.mode) : '';
219
- return {
220
- operation_id: op.operation_id, identifier: op.identifier, method: op.method, level: op.level,
221
- description: op.description, permissionId: perm?.permission_id, free,
222
- control: free
223
- ? new FormControl({ value: freeChecked(this.mode), disabled: true }, { nonNullable: true })
224
- : new FormControl(!!perm, { nonNullable: true }),
225
- };
226
- });
297
+ const segments = String(op.identifier).split(':');
298
+ const leafName = segments.pop();
299
+ let level = root;
300
+ let path = '';
301
+ for (const segment of segments) {
302
+ path = path ? `${path}:${segment}` : segment;
303
+ let folder = level.find(n => n.isFolder && n.name === segment);
304
+ if (!folder) {
305
+ // Chave da pasta termina em ':' para não colidir com uma operação de mesmo identifier ("orders" e "orders:list").
306
+ folder = { key: `${path}:`, name: segment, isFolder: true, children: [] };
307
+ level.push(folder);
308
+ }
309
+ level = folder.children;
310
+ }
311
+ level.push({
312
+ key: op.identifier, name: leafName, isFolder: false, children: [], identifier: op.identifier,
313
+ operation_id: op.operation_id, method: op.method, level: op.level, description: op.description,
314
+ permissionId: perm?.permission_id,
315
+ locked: !perm && !requiresPermission(op.level, this.mode),
316
+ });
317
+ }
318
+ this.tree.set(root);
319
+ const original = new Set(collectAll(root).filter(l => !l.locked && l.permissionId).map(l => l.key));
320
+ this.originalSelectedIds = original;
321
+ this.selectedIds.set(new Set(original));
322
+ this.expandedIds.set(new Set());
227
323
  }
228
324
  finally {
229
325
  this.loading = false;
230
326
  this.cdr.markForCheck();
231
327
  }
232
328
  }
233
- get hasChanges() { return this.rows.some(r => !r.free && r.control.value !== !!r.permissionId); }
329
+ /**
330
+ * Mudança do hm-tree: com caixas ele emite só folhas habilitadas (ou o id da pasta sem folhas). Pasta vira as folhas
331
+ * escolhíveis dela; folha travada e chave desconhecida são ignoradas.
332
+ */
333
+ onTreeChanged(changes) {
334
+ this.selectedIds.update(current => {
335
+ const next = new Set(current);
336
+ for (const change of changes) {
337
+ const node = this.nodesByKey().get(change.id);
338
+ if (!node)
339
+ continue;
340
+ for (const leaf of collectLeaves(node)) {
341
+ if (leaf.locked)
342
+ continue;
343
+ if (change.selected)
344
+ next.add(leaf.key);
345
+ else
346
+ next.delete(leaf.key);
347
+ }
348
+ }
349
+ return next;
350
+ });
351
+ }
352
+ get hasChanges() {
353
+ const selected = this.selectedIds();
354
+ if (selected.size !== this.originalSelectedIds.size)
355
+ return true;
356
+ for (const id of selected)
357
+ if (!this.originalSelectedIds.has(id))
358
+ return true;
359
+ return false;
360
+ }
361
+ /** Folha escolhível pela chave; `undefined` para pasta, operação travada ou chave desconhecida. */
362
+ leafByKey(key) {
363
+ const node = this.nodesByKey().get(key);
364
+ return node && !node.isFolder && !node.locked ? node : undefined;
365
+ }
234
366
  async confirm() {
235
- const toAdd = this.rows.filter(r => !r.free && r.control.value && !r.permissionId);
236
- const toRemove = this.rows.filter(r => !r.free && !r.control.value && r.permissionId);
367
+ const selected = this.selectedIds();
368
+ const isLeaf = (l) => !!l;
369
+ const toAdd = [...selected].filter(id => !this.originalSelectedIds.has(id)).map(id => this.leafByKey(id)).filter(isLeaf);
370
+ const toRemove = [...this.originalSelectedIds].filter(id => !selected.has(id)).map(id => this.leafByKey(id)).filter(isLeaf);
237
371
  if (toAdd.length === 0 && toRemove.length === 0) {
238
372
  this.close();
239
373
  return;
240
374
  }
241
375
  this.saving = true;
242
376
  try {
243
- for (const r of toAdd) {
244
- await this.service.addAppPermission(this.clientId, this.consumer.consumer_id, r.operation_id);
377
+ for (const l of toAdd) {
378
+ await this.service.addAppPermission(this.clientId, this.consumer.consumer_id, l.operation_id);
245
379
  }
246
- for (const r of toRemove) {
247
- await this.service.removeAppPermission(this.clientId, this.consumer.consumer_id, r.permissionId);
380
+ for (const l of toRemove) {
381
+ await this.service.removeAppPermission(this.clientId, this.consumer.consumer_id, l.permissionId);
248
382
  }
249
383
  this.close();
250
384
  }
@@ -258,11 +392,11 @@ class AutoServiceConsumerPermissionsComponent {
258
392
  }
259
393
  }
260
394
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceConsumerPermissionsComponent, deps: [{ token: AutoServiceService }], target: i0.ɵɵFactoryTarget.Component }); }
261
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.24", type: AutoServiceConsumerPermissionsComponent, isStandalone: true, selector: "spine-auto-service-consumer-permissions", inputs: { clientId: "clientId", appId: "appId", consumer: "consumer", mode: "mode", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<hm-drawer [open]=\"open\" (openChange)=\"open = $event\" [heading]=\"fmt(text.operationsOf, { group: consumer?.group_name ?? '' })\" (closed)=\"close()\">\n <div *ngIf=\"loading\" class=\"loading-row\">{{ text.loading }}</div>\n <p class=\"op-note muted\" *ngIf=\"!loading && mode === 'token' && rows.length\">{{ text.tokenOperationsNote }}</p>\n\n <div class=\"op-list\" *ngIf=\"!loading\">\n <label class=\"op-row\" *ngFor=\"let row of rows\">\n <input type=\"checkbox\" class=\"hm-check\" [formControl]=\"row.control\" />\n <span class=\"op-row__body\">\n <code>{{ row.identifier }}</code>\n <hm-tag [name]=\"row.method\" />\n <hm-tag [name]=\"row.level\" [color]=\"levelTagColor(row.level)\" />\n <span class=\"op-row__free\" *ngIf=\"row.free\">{{ row.free }}</span>\n <span class=\"op-row__desc muted\" *ngIf=\"row.description\">{{ row.description }}</span>\n </span>\n </label>\n </div>\n\n <div class=\"empty-state\" *ngIf=\"!loading && rows.length === 0\">\n <hm-icon shape=\"filter-grid\" size=\"xl\"></hm-icon>\n <p>{{ text.noOperations }}</p>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!hasChanges\" [loading]=\"saving\" (clicked)=\"confirm()\">{{ text.confirmChanges }}</hm-button>\n </ng-container>\n</hm-drawer>\n", styles: ["@charset \"UTF-8\";.as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.loading-row{display:flex;justify-content:center;padding:var(--hm-space-xl);color:var(--hm-muted)}.op-note{margin:0 0 var(--hm-space-sm);font-size:var(--hm-font-size-sm)}.op-list{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.op-row{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-xs);border-radius:var(--hm-radius-control);cursor:pointer}.op-row:hover{background:var(--hm-hover)}.op-row input[type=checkbox]{margin-top:var(--hm-space-micro)}.op-row__body{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.op-row__free{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-style:italic}.op-row__desc{flex-basis:100%;font-size:var(--hm-font-size-xs)}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-xl);color:var(--hm-muted);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i3.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
395
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: AutoServiceConsumerPermissionsComponent, isStandalone: true, selector: "spine-auto-service-consumer-permissions", inputs: { clientId: "clientId", appId: "appId", consumer: "consumer", mode: "mode", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<hm-drawer [open]=\"open\" (openChange)=\"open = $event\" [heading]=\"fmt(text.operationsOf, { group: consumer?.group_name ?? '' })\" (closed)=\"close()\">\n <div *ngIf=\"loading\" class=\"loading-row\">{{ text.loading }}</div>\n\n <ng-container *ngIf=\"!loading && treeNodes().length\">\n <!-- Grupo: s\u00F3 protected depende de permiss\u00E3o; public/private/optional j\u00E1 valem e aparecem marcadas e desabilitadas.\n Token: private/optional/protected s\u00F3 valem liberadas; public aparece desabilitada e desmarcada. -->\n <p class=\"cp-summary\">{{ summary() }} {{ freeSummary() }}</p>\n\n <hm-tree\n [nodes]=\"treeNodes()\"\n [label]=\"text.permissionsTreeLabel\"\n [selected]=\"treeSelected()\"\n [(expanded)]=\"expandedIds\"\n (changed)=\"onTreeChanged($event)\"\n >\n <ng-template hmTreeNode let-node>\n @let item = info(node);\n <span class=\"cp-node\">\n <span class=\"cp-node__name\">{{ item.name }}</span>\n @if (item.isFolder) {\n @if (item.count) {\n <span class=\"cp-node__count\">{{ item.count }}</span>\n }\n } @else {\n <hm-tag [name]=\"item.method ?? ''\" color=\"neutral\" />\n <hm-tag [name]=\"item.level ?? ''\" [color]=\"item.levelColor ?? 'neutral'\" />\n @if (item.description) {\n <span class=\"cp-node__desc\">{{ item.description }}</span>\n }\n }\n </span>\n </ng-template>\n </hm-tree>\n </ng-container>\n\n <div class=\"empty-state\" *ngIf=\"!loading && treeNodes().length === 0\">\n <hm-icon shape=\"filter-grid\" size=\"xl\"></hm-icon>\n <p>{{ text.noOperations }}</p>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!hasChanges\" [loading]=\"saving\" (clicked)=\"confirm()\">{{ text.confirmChanges }}</hm-button>\n </ng-container>\n</hm-drawer>\n", styles: ["@charset \"UTF-8\";.as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.loading-row{display:flex;justify-content:center;padding:var(--hm-space-xl);color:var(--hm-muted)}.cp-summary{margin:0 0 var(--hm-space-sm);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.cp-node{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-width:0}.cp-node__count,.cp-node__desc{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-xl);color:var(--hm-muted);text-align:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "busy", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: DrawerComponent, selector: "hm-drawer", inputs: ["open", "heading", "size", "position", "pinned", "closeLabel", "closable", "beforeClose"], outputs: ["openChange", "closed"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: TagComponent, selector: "hm-tag", inputs: ["name", "color", "compact"] }, { kind: "component", type: TreeComponent, selector: "hm-tree", inputs: ["nodes", "label", "emptyMessage", "selectable", "selectionMode", "selection", "selected", "expanded", "activeId", "loadChildren", "draggable", "canDrop", "nodeActions"], outputs: ["selectionChange", "expandedChange", "activeIdChange", "changed", "activated", "moved", "nodeAction"] }, { kind: "directive", type: TreeNodeTemplateDirective, selector: "ng-template[hmTreeNode]" }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
262
396
  }
263
397
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: AutoServiceConsumerPermissionsComponent, decorators: [{
264
398
  type: Component,
265
- args: [{ selector: 'spine-auto-service-consumer-permissions', standalone: true, imports: [CommonModule, ReactiveFormsModule, ButtonComponent, DrawerComponent, IconComponent, TagComponent], changeDetection: ChangeDetectionStrategy.Eager, template: "<hm-drawer [open]=\"open\" (openChange)=\"open = $event\" [heading]=\"fmt(text.operationsOf, { group: consumer?.group_name ?? '' })\" (closed)=\"close()\">\n <div *ngIf=\"loading\" class=\"loading-row\">{{ text.loading }}</div>\n <p class=\"op-note muted\" *ngIf=\"!loading && mode === 'token' && rows.length\">{{ text.tokenOperationsNote }}</p>\n\n <div class=\"op-list\" *ngIf=\"!loading\">\n <label class=\"op-row\" *ngFor=\"let row of rows\">\n <input type=\"checkbox\" class=\"hm-check\" [formControl]=\"row.control\" />\n <span class=\"op-row__body\">\n <code>{{ row.identifier }}</code>\n <hm-tag [name]=\"row.method\" />\n <hm-tag [name]=\"row.level\" [color]=\"levelTagColor(row.level)\" />\n <span class=\"op-row__free\" *ngIf=\"row.free\">{{ row.free }}</span>\n <span class=\"op-row__desc muted\" *ngIf=\"row.description\">{{ row.description }}</span>\n </span>\n </label>\n </div>\n\n <div class=\"empty-state\" *ngIf=\"!loading && rows.length === 0\">\n <hm-icon shape=\"filter-grid\" size=\"xl\"></hm-icon>\n <p>{{ text.noOperations }}</p>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!hasChanges\" [loading]=\"saving\" (clicked)=\"confirm()\">{{ text.confirmChanges }}</hm-button>\n </ng-container>\n</hm-drawer>\n", styles: ["@charset \"UTF-8\";.as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.loading-row{display:flex;justify-content:center;padding:var(--hm-space-xl);color:var(--hm-muted)}.op-note{margin:0 0 var(--hm-space-sm);font-size:var(--hm-font-size-sm)}.op-list{display:flex;flex-direction:column;gap:var(--hm-space-micro)}.op-row{display:flex;align-items:flex-start;gap:var(--hm-space-sm);padding:var(--hm-space-xs);border-radius:var(--hm-radius-control);cursor:pointer}.op-row:hover{background:var(--hm-hover)}.op-row input[type=checkbox]{margin-top:var(--hm-space-micro)}.op-row__body{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.op-row__free{color:var(--hm-muted);font-size:var(--hm-font-size-xs);font-style:italic}.op-row__desc{flex-basis:100%;font-size:var(--hm-font-size-xs)}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-xl);color:var(--hm-muted);text-align:center}\n"] }]
399
+ args: [{ selector: 'spine-auto-service-consumer-permissions', standalone: true, imports: [CommonModule, ButtonComponent, DrawerComponent, IconComponent, TagComponent, TreeComponent, TreeNodeTemplateDirective], changeDetection: ChangeDetectionStrategy.Eager, template: "<hm-drawer [open]=\"open\" (openChange)=\"open = $event\" [heading]=\"fmt(text.operationsOf, { group: consumer?.group_name ?? '' })\" (closed)=\"close()\">\n <div *ngIf=\"loading\" class=\"loading-row\">{{ text.loading }}</div>\n\n <ng-container *ngIf=\"!loading && treeNodes().length\">\n <!-- Grupo: s\u00F3 protected depende de permiss\u00E3o; public/private/optional j\u00E1 valem e aparecem marcadas e desabilitadas.\n Token: private/optional/protected s\u00F3 valem liberadas; public aparece desabilitada e desmarcada. -->\n <p class=\"cp-summary\">{{ summary() }} {{ freeSummary() }}</p>\n\n <hm-tree\n [nodes]=\"treeNodes()\"\n [label]=\"text.permissionsTreeLabel\"\n [selected]=\"treeSelected()\"\n [(expanded)]=\"expandedIds\"\n (changed)=\"onTreeChanged($event)\"\n >\n <ng-template hmTreeNode let-node>\n @let item = info(node);\n <span class=\"cp-node\">\n <span class=\"cp-node__name\">{{ item.name }}</span>\n @if (item.isFolder) {\n @if (item.count) {\n <span class=\"cp-node__count\">{{ item.count }}</span>\n }\n } @else {\n <hm-tag [name]=\"item.method ?? ''\" color=\"neutral\" />\n <hm-tag [name]=\"item.level ?? ''\" [color]=\"item.levelColor ?? 'neutral'\" />\n @if (item.description) {\n <span class=\"cp-node__desc\">{{ item.description }}</span>\n }\n }\n </span>\n </ng-template>\n </hm-tree>\n </ng-container>\n\n <div class=\"empty-state\" *ngIf=\"!loading && treeNodes().length === 0\">\n <hm-icon shape=\"filter-grid\" size=\"xl\"></hm-icon>\n <p>{{ text.noOperations }}</p>\n </div>\n\n <ng-container footer>\n <hm-button emphasis=\"medium\" (clicked)=\"close()\">{{ text.cancel }}</hm-button>\n <hm-button [disabled]=\"!hasChanges\" [loading]=\"saving\" (clicked)=\"confirm()\">{{ text.confirmChanges }}</hm-button>\n </ng-container>\n</hm-drawer>\n", styles: ["@charset \"UTF-8\";.as-stack{display:flex;flex-direction:column;gap:var(--hm-space-md);min-inline-size:0}.as-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.as-field-row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--hm-space-sm)}.as-field-row>hm-combobox{flex:1 1 16rem;min-inline-size:0}.as-summary{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-sm)}.as-summary__info{display:flex;flex:1 1 16rem;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.as-summary__title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs);margin:0;color:var(--hm-text);font-size:var(--hm-font-size-md);font-weight:var(--hm-font-weight-semibold)}.as-summary__detail{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.muted{color:var(--hm-muted)}.as-empty{margin:var(--hm-space-xs) 0 0;font-size:var(--hm-font-size-sm)}.as-secret{display:grid;gap:var(--hm-space-micro)}.as-secret__label{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.as-secret__row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.as-secret__value{flex:1 1 240px;min-width:0;border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:var(--hm-radius-control);padding:var(--hm-space-xs) var(--hm-space-sm);background:var(--hm-surface);color:var(--hm-text);font-family:var(--hm-font-mono);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-user-select:all;user-select:all}.loading-row{display:flex;justify-content:center;padding:var(--hm-space-xl);color:var(--hm-muted)}.cp-summary{margin:0 0 var(--hm-space-sm);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.cp-node{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xxxs) var(--hm-space-xs);min-width:0}.cp-node__count,.cp-node__desc{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.empty-state{display:flex;flex-direction:column;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-xl);color:var(--hm-muted);text-align:center}\n"] }]
266
400
  }], ctorParameters: () => [{ type: AutoServiceService }], propDecorators: { clientId: [{
267
401
  type: Input
268
402
  }], appId: [{
@@ -276,6 +410,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
276
410
  }], openChange: [{
277
411
  type: Output
278
412
  }] } });
413
+ function collectLeaves(node) {
414
+ return node.isFolder ? node.children.flatMap(collectLeaves) : [node];
415
+ }
416
+ function collectAll(root) {
417
+ return root.flatMap(collectLeaves);
418
+ }
279
419
 
280
420
  class AutoServiceAppAccessComponent {
281
421
  constructor(service) {
@@ -938,4 +1078,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
938
1078
  }] });
939
1079
 
940
1080
  export { AutoServiceComponent };
941
- //# sourceMappingURL=trismegisto-spine-auto-service-auto-service.component-DV88Uger.mjs.map
1081
+ //# sourceMappingURL=trismegisto-spine-auto-service-auto-service.component-DAXv__zg.mjs.map