csrfToken corregido
This commit is contained in:
parent
dd64a01622
commit
d8a09888e8
9
.lintrc
9
.lintrc
@ -90,6 +90,15 @@
|
||||
"function-overload",
|
||||
"function"
|
||||
]
|
||||
},
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": {
|
||||
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
|
||||
},
|
||||
"next": {
|
||||
"selector": "ExportNamedDeclaration[source], ExportAllDeclaration"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@stylistic/lines-between-class-members": [
|
||||
|
||||
@ -26,7 +26,6 @@ export function App() {
|
||||
renderWorkspace={(snapshot, accountNavigation) => (
|
||||
<CompaniesWorkspace
|
||||
accountNavigation={accountNavigation}
|
||||
csrfToken={snapshot.csrfToken}
|
||||
languageSwitcher={<LanguageSwitcher />}
|
||||
userName={snapshot.user?.name ?? ""}
|
||||
/>
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { coreSpanish } from "@metis/core/client";
|
||||
import { configureCsrfTokenReader, coreSpanish } from "@metis/core/client";
|
||||
import { identityKeys } from "@metis/identity/client";
|
||||
import type { SessionResponseDTO } from "@metis/identity/common";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
@ -16,6 +18,10 @@ if (!rootElement) {
|
||||
const { instance, initialization } = createAppI18n();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
configureCsrfTokenReader(
|
||||
() => queryClient.getQueryData<SessionResponseDTO>(identityKeys.session)?.csrfToken
|
||||
);
|
||||
|
||||
initialization
|
||||
.then(() => {
|
||||
createRoot(rootElement).render(
|
||||
|
||||
73
docs/centralizacion-csrf-y-prop-drilling.md
Normal file
73
docs/centralizacion-csrf-y-prop-drilling.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Centralización de CSRF y revisión de prop drilling
|
||||
|
||||
## Cambio aplicado
|
||||
|
||||
El token CSRF pertenece a la sesión HTTP, no a los formularios ni a las entidades.
|
||||
`apps/web/src/main.tsx` configura una función que consulta la sesión actual en la caché
|
||||
de TanStack Query (`identityKeys.session`). `modules/core/src/web/http/shared/api/csrf-fetch.ts`
|
||||
lee esa función en cada petición de escritura y añade `x-csrf-token`.
|
||||
|
||||
No existe una segunda copia del token en React, localStorage o un contexto. La caché de
|
||||
sesión sigue siendo la fuente única. GET/HEAD/OPTIONS permiten obtener la sesión sin token;
|
||||
POST y las demás escrituras requieren el token hexadecimal de 64 caracteres que genera
|
||||
el servidor. Un token ausente o inválido impide enviar la petición. El transporte solo
|
||||
acepta rutas locales `/api/` y utiliza credenciales del mismo origen.
|
||||
|
||||
Se han eliminado `csrfToken` de props, controladores, callbacks y firmas de los clientes
|
||||
HTTP de identity, companies, assets y resources. Las peticiones de fotos y logotipos usan
|
||||
el mismo transporte, conservando su cuerpo binario. El contrato HTTP, las validaciones
|
||||
de respuestas y errores de cada módulo y la protección del backend se mantienen.
|
||||
|
||||
La renovación de sesión tras login, configuración inicial, cambio de contraseña y logout
|
||||
continúa utilizando las invalidaciones existentes. La siguiente petición lee el token que
|
||||
haya en la caché en ese momento; no captura el token de un render anterior. Un 403 no se
|
||||
reintenta automáticamente, para no repetir escrituras. Las pruebas aisladas configuran y
|
||||
restauran su lector mediante la función de limpieza de `configureCsrfTokenReader`.
|
||||
|
||||
## Criterio de análisis
|
||||
|
||||
Se revisaron las props y su recorrido entre workspace, páginas, diálogos, formularios,
|
||||
controles y hooks de los cinco módulos y la aplicación web. Una prop transmitida a un
|
||||
hijo directo no constituye por sí sola un problema: importa si las capas intermedias solo
|
||||
la retransmiten, si tiene una fuente compartida y si su alcance es local o global.
|
||||
|
||||
La corrección de las demás propiedades queda pendiente: esta tarea implementa CSRF y
|
||||
analiza el resto, sin modificar sus contratos funcionales.
|
||||
|
||||
## Casos encontrados y recomendaciones
|
||||
|
||||
| Propiedades | Recorrido real representativo | Valoración y siguiente paso |
|
||||
| --- | --- | --- |
|
||||
| `company` / `companyId` | `CompanyDetail` → `CompanySection` → `WorkProfilesPage` → `WorkProfileEditorDialog` → `WorkProfileForm`; también las páginas de configuraciones y sus formularios | Prioridad alta. Un contexto limitado al espacio de la compañía podría aportar el identificador y los datos ya consultados. No crear estado duplicado ni hacer que formularios reutilizables dependan obligatoriamente de ese contexto. Conservar el identificador explícito en los clientes HTTP y en los listados globales. |
|
||||
| `lookups` | `CompanyInventory` → `AssetInventoryPage` → `AssetEditorDialog` → `AssetForm`; también `WorkstationsPage` → `WorkstationEditorDialog` → `WorkstationForm` → `WorkstationAssetCreateDialog` | Prioridad media. Mezcla datos de sedes/personas con callbacks de alta rápida entre companies y assets. Separar ambos contratos antes de reducir su paso. Un proveedor limitado al inventario puede compartir datos; las acciones de alta deben seguir siendo aportadas por el módulo propietario. |
|
||||
| `userName` | `App` → `CompaniesWorkspace` → `WorkspaceHeader` | Prioridad baja. Dato de sesión pasado a través de una capa. Un hook público de identidad puede ofrecer el usuario actual a la cabecera si aumenta su uso. Hoy solo hay una capa intermediaria: no justifica por sí sola un contexto adicional. |
|
||||
| `scope`, `items`, plantillas y catálogos | Página de catálogo/inventario → diálogo de edición → formulario/controles | Datos y ámbito de la operación, en su mayoría consumidos también por las capas intermedias. Mantener explícitos; si varios consumidores consultan lo mismo, reutilizar hooks y claves de Query existentes. `scope` puede ser global o compañía: no convertirlo en una compañía implícita. |
|
||||
| `onSaved`, `onClose`, `onCreated`, `onChange` | Página → diálogo → formulario/control | Contratos de comunicación local. Mantener: controlan cierre, selección del registro recién creado e invalidación. No reemplazarlos por un bus global. |
|
||||
| `allowQuickCreate` | Formulario → control de marca/plantilla; formularios anidados establecen `false` | Opción local deliberada para limitar las altas anidadas. Mantener explícita para que el diálogo hijo no abra niveles adicionales por herencia accidental. |
|
||||
| `accountNavigation`, `languageSwitcher`, `children`, `relatedContent` | `App` → `CompaniesWorkspace` → cabecera/configuración; render props de secciones | Composición de interfaz entre módulos. Son slots, no estado duplicado. Mantener para preservar independencia de módulos y evitar importar identity desde companies. |
|
||||
| `initialId`, `initialCreate`, navegación y filtros iniciales | Workspace → sección/página → controlador | Traducción de ruta a estado de pantalla. La capa intermedia suele interpretar la ruta. Mantener la responsabilidad del routing; reducir solo retransmisiones innecesarias al trabajar en cada pantalla. |
|
||||
|
||||
## Orden sugerido
|
||||
|
||||
1. Revisar el alcance de compañía dentro de `CompanyDetail`, sin afectar pantallas globales.
|
||||
2. Separar datos y acciones de `lookups` y evaluar un proveedor de inventario acotado.
|
||||
3. Extraer un hook de usuario actual únicamente si más consumidores necesitan la sesión.
|
||||
|
||||
No crear un contexto global con todas las props: ocultaría dependencias de formularios,
|
||||
complicaría las pruebas y podría mezclar ámbitos de compañías distintas.
|
||||
|
||||
## Verificación
|
||||
|
||||
- Pruebas del transporte: token leído de Query, rotación, borrado de caché, ausencia/invalidez,
|
||||
lectura sin sesión, destino local, cuerpos JSON/binarios, headers, cancelación y ausencia
|
||||
de reintentos de escrituras fallidas.
|
||||
- Pruebas de contratos de identity, accesos, buzones y marcas adaptadas al transporte.
|
||||
- Suite completa del monorepo: 277 pruebas correctas, incluida integración HTTP con MariaDB.
|
||||
- Pruebas de espaciado: 3 correctas. Los reexports consecutivos pueden mantenerse agrupados
|
||||
para que ESLint y la ordenación de exports de Biome no se contradigan.
|
||||
- Build de producción: correcto.
|
||||
- Biome y reglas de espaciado sobre ficheros modificados: correctos.
|
||||
- Typecheck del módulo core: correcto. Las comprobaciones generales de tipos/lint/check
|
||||
encuentran errores previos fuera del cambio:
|
||||
`calendar.tsx` de vendor, `personId` antiguo en el fixture web y reglas de llaves en
|
||||
código de fotos. No se modificó vendor ni se amplió el alcance del refactor para ellos.
|
||||
@ -2,12 +2,8 @@ import type { RelationshipInputDTO } from "../../../../common/relationships";
|
||||
import { RelationshipResponseSchema } from "../../../../common/relationships";
|
||||
import { inventoryRequest } from "../../../shared/api/inventory-request";
|
||||
|
||||
export async function createAssetRelationship(
|
||||
companyId: string,
|
||||
values: RelationshipInputDTO,
|
||||
token: string
|
||||
) {
|
||||
export async function createAssetRelationship(companyId: string, values: RelationshipInputDTO) {
|
||||
return RelationshipResponseSchema.parse(
|
||||
await inventoryRequest(`${companyId}/relationships`, { values }, token)
|
||||
await inventoryRequest(`${companyId}/relationships`, { values })
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { RelationshipResponseSchema } from "../../../../common/relationships";
|
||||
import { inventoryRequest } from "../../../shared/api/inventory-request";
|
||||
|
||||
export async function endAssetRelationship(companyId: string, id: string, token: string) {
|
||||
export async function endAssetRelationship(companyId: string, id: string) {
|
||||
return RelationshipResponseSchema.parse(
|
||||
await inventoryRequest(`${companyId}/relationships/${id}/end`, {}, token)
|
||||
await inventoryRequest(`${companyId}/relationships/${id}/end`, {})
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,11 +7,10 @@ import type { EndRelationshipDialogProps } from "../types";
|
||||
export function useEndRelationshipDialogController({
|
||||
relationship,
|
||||
companyId,
|
||||
csrfToken,
|
||||
}: EndRelationshipDialogProps) {
|
||||
const client = useQueryClient();
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => endAssetRelationship(companyId, relationship.id, csrfToken),
|
||||
mutationFn: () => endAssetRelationship(companyId, relationship.id),
|
||||
onSuccess: () => client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import type { RelationshipDTO } from "../../../../common";
|
||||
export type EndRelationshipDialogProps = {
|
||||
relationship: RelationshipDTO;
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
sourceName: string;
|
||||
targetName: string;
|
||||
onClose: () => void;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export type AssetRelationshipsProps = {
|
||||
compact?: boolean;
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
assetId: string;
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ import { useAssetRelationshipsController } from "../controllers";
|
||||
import type { AssetRelationshipsProps } from "../types";
|
||||
|
||||
export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
const { companyId, csrfToken, assetId, compact = false } = props;
|
||||
const { companyId, assetId, compact = false } = props;
|
||||
const { query, adding, setAdding, setEndingId, ending, active, ended, dateOf } =
|
||||
useAssetRelationshipsController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
@ -85,7 +85,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
)}
|
||||
<DeleteRecordDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onClose={() => setDeleteId(undefined)}
|
||||
path={deleteId ? `/api/assets/${companyId}/relationships/${deleteId}/delete` : undefined}
|
||||
queryKey={["inventory", companyId]}
|
||||
@ -94,7 +93,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
<RelationshipEditorDialog
|
||||
assetId={assetId}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onOpenChange={setAdding}
|
||||
onSaved={() => setAdding(false)}
|
||||
/>
|
||||
@ -102,7 +100,6 @@ export function AssetRelationships(props: AssetRelationshipsProps) {
|
||||
{ending && (
|
||||
<EndRelationshipDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onClose={() => setEndingId(undefined)}
|
||||
relationship={ending}
|
||||
sourceName={nameOf(ending.values.sourceId)}
|
||||
|
||||
@ -11,7 +11,6 @@ import type { RelationshipFormProps } from "../types";
|
||||
|
||||
export function useRelationshipFormController({
|
||||
companyId,
|
||||
csrfToken,
|
||||
assetId,
|
||||
onSaved,
|
||||
}: RelationshipFormProps) {
|
||||
@ -29,8 +28,7 @@ export function useRelationshipFormController({
|
||||
queryFn: () => getAssetInventory(companyId),
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: RelationshipInputDTO) =>
|
||||
createAssetRelationship(companyId, values, csrfToken),
|
||||
mutationFn: (values: RelationshipInputDTO) => createAssetRelationship(companyId, values),
|
||||
onSuccess: async () => {
|
||||
await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) });
|
||||
onSaved();
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
export type RelationshipFormProps = {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
assetId: string;
|
||||
onSaved: () => void;
|
||||
};
|
||||
|
||||
@ -6,13 +6,11 @@ import { RelationshipForm } from "./relationship-form";
|
||||
export function RelationshipEditorDialog({
|
||||
companyId,
|
||||
assetId,
|
||||
csrfToken,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
companyId: string;
|
||||
assetId: string;
|
||||
csrfToken: string;
|
||||
onSaved: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
@ -26,12 +24,7 @@ export function RelationshipEditorDialog({
|
||||
open
|
||||
title={t("linkAsset")}
|
||||
>
|
||||
<RelationshipForm
|
||||
assetId={assetId}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onSaved={onSaved}
|
||||
/>
|
||||
<RelationshipForm assetId={assetId} companyId={companyId} onSaved={onSaved} />
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { useRelationshipFormController } from "../controllers";
|
||||
import type { RelationshipFormProps } from "../types";
|
||||
|
||||
export function RelationshipForm(props: RelationshipFormProps) {
|
||||
const { companyId, csrfToken } = props;
|
||||
const { companyId } = props;
|
||||
const {
|
||||
typeId,
|
||||
setTypeId,
|
||||
@ -94,7 +94,6 @@ export function RelationshipForm(props: RelationshipFormProps) {
|
||||
{quick && (
|
||||
<CatalogRelationCreateDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onOpenChange={setQuick}
|
||||
onSaved={(savedId) => {
|
||||
setTypeId(savedId);
|
||||
|
||||
@ -6,14 +6,12 @@ import { AssetForm } from "../../shared";
|
||||
|
||||
export function WorkstationAssetCreateDialog({
|
||||
companyId,
|
||||
csrfToken,
|
||||
items,
|
||||
lookups,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
items: ComponentProps<typeof AssetForm>["items"];
|
||||
lookups: ComponentProps<typeof AssetForm>["lookups"];
|
||||
onSaved: (id: string) => void;
|
||||
@ -34,7 +32,6 @@ export function WorkstationAssetCreateDialog({
|
||||
<AssetForm
|
||||
allowQuickCreate={false}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
items={items}
|
||||
lookups={lookups}
|
||||
onSaved={onSaved}
|
||||
|
||||
@ -11,13 +11,11 @@ export function AssetDetailDialog({
|
||||
asset,
|
||||
lookups,
|
||||
companyId,
|
||||
csrfToken,
|
||||
onClose,
|
||||
}: {
|
||||
asset: ComponentProps<typeof AssetDetail>["asset"];
|
||||
lookups: ComponentProps<typeof AssetDetail>["lookups"];
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("assets");
|
||||
@ -35,11 +33,10 @@ export function AssetDetailDialog({
|
||||
wide
|
||||
>
|
||||
<AssetDetail asset={asset} lookups={lookups}>
|
||||
<AssetRelationships assetId={asset.id} companyId={companyId} csrfToken={csrfToken} />
|
||||
<AssetRelationships assetId={asset.id} companyId={companyId} />
|
||||
<AssetSupport
|
||||
assetId={asset.id}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
retired={asset.values.status === "retired"}
|
||||
/>
|
||||
</AssetDetail>
|
||||
|
||||
@ -12,7 +12,6 @@ import { AssetDetail } from "./asset-detail";
|
||||
|
||||
export function AssetSelectionPanel({
|
||||
companyId,
|
||||
csrfToken,
|
||||
asset,
|
||||
open,
|
||||
lookups,
|
||||
@ -22,7 +21,6 @@ export function AssetSelectionPanel({
|
||||
onEdit,
|
||||
}: {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
asset: AssetDTO | undefined;
|
||||
open: boolean;
|
||||
lookups: InventoryLookups;
|
||||
@ -58,19 +56,13 @@ export function AssetSelectionPanel({
|
||||
</div>
|
||||
<AssetDetail asset={asset} compact lookups={lookups}>
|
||||
<DetailSection title={t("relations")}>
|
||||
<AssetRelationships
|
||||
assetId={asset.id}
|
||||
compact
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
/>
|
||||
<AssetRelationships assetId={asset.id} compact companyId={companyId} />
|
||||
</DetailSection>
|
||||
<DetailSection title={t("support.title")}>
|
||||
<AssetSupport
|
||||
assetId={asset.id}
|
||||
compact
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
retired={asset.values.status === "retired"}
|
||||
/>
|
||||
</DetailSection>
|
||||
|
||||
@ -2,7 +2,6 @@ import type { InventoryLookups } from "../../shared/types";
|
||||
|
||||
export type AssetInventoryPageProps = {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
lookups: InventoryLookups;
|
||||
initialId?: string | undefined;
|
||||
initialCreate?: boolean | undefined;
|
||||
|
||||
@ -23,7 +23,7 @@ function selectedRowClass(id: string, selectedId: string | undefined, detailOpen
|
||||
}
|
||||
|
||||
export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
||||
const { companyId, csrfToken, lookups } = props;
|
||||
const { companyId, lookups } = props;
|
||||
const {
|
||||
query,
|
||||
catalog,
|
||||
@ -177,7 +177,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
||||
asset={selected}
|
||||
canEdit={catalog.isSuccess}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
lookups={lookups}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
onEdit={setEditor}
|
||||
@ -188,7 +187,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
||||
<AssetEditorDialog
|
||||
asset={edited}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
items={catalog.data.items}
|
||||
lookups={lookups}
|
||||
onClose={() => setEditor(undefined)}
|
||||
@ -203,7 +201,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) {
|
||||
<AssetDetailDialog
|
||||
asset={viewed}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
lookups={lookups}
|
||||
onClose={() => setViewId(undefined)}
|
||||
/>
|
||||
|
||||
@ -2,8 +2,8 @@ import type { AssetInput } from "../../../../common/inventory";
|
||||
import { AssetResponseSchema } from "../../../../common/inventory";
|
||||
import { inventoryRequest } from "../../../shared/api/inventory-request";
|
||||
|
||||
export async function saveAsset(companyId: string, values: AssetInput, token: string, id?: string) {
|
||||
export async function saveAsset(companyId: string, values: AssetInput, id?: string) {
|
||||
return AssetResponseSchema.parse(
|
||||
await inventoryRequest(`${companyId}${id ? `/${id}/update` : ""}`, { values }, token)
|
||||
await inventoryRequest(`${companyId}${id ? `/${id}/update` : ""}`, { values })
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,13 +9,7 @@ import { inventoryKeys } from "../../../shared/queries";
|
||||
import { saveAsset } from "../api";
|
||||
import type { AssetFormProps } from "../types";
|
||||
|
||||
export function useAssetFormController({
|
||||
companyId,
|
||||
csrfToken,
|
||||
asset,
|
||||
items,
|
||||
onSaved,
|
||||
}: AssetFormProps) {
|
||||
export function useAssetFormController({ companyId, asset, items, onSaved }: AssetFormProps) {
|
||||
const client = useQueryClient();
|
||||
const [creatingTemplate, setCreatingTemplate] = useState(false);
|
||||
const mounted = useRef(true);
|
||||
@ -68,7 +62,7 @@ export function useAssetFormController({
|
||||
(template?.values.kind === "template" ? template.values.sections : undefined) ??
|
||||
[];
|
||||
const mutation = useMutation({
|
||||
mutationFn: (input: AssetInput) => saveAsset(companyId, input, csrfToken, asset?.id),
|
||||
mutationFn: (input: AssetInput) => saveAsset(companyId, input, asset?.id),
|
||||
onSuccess: async (response) => {
|
||||
await Promise.all([
|
||||
client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),
|
||||
|
||||
@ -67,7 +67,7 @@ test("Inventario traducido con alta de cabecera y sin formulario junto al listad
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await render(
|
||||
language,
|
||||
<AssetInventoryPage companyId={companyId} csrfToken="test" lookups={lookups} />
|
||||
<AssetInventoryPage companyId={companyId} lookups={lookups} />
|
||||
);
|
||||
|
||||
assert.equal(html.includes("<form"), false);
|
||||
@ -84,13 +84,7 @@ test("Inventario traducido con alta de cabecera y sin formulario junto al listad
|
||||
test("El activo ofrece altas relacionadas, ayuda inferior y no encadena una tercera alta", async () => {
|
||||
const html = await render(
|
||||
"es",
|
||||
<AssetForm
|
||||
companyId={companyId}
|
||||
csrfToken="test"
|
||||
items={[]}
|
||||
lookups={lookups}
|
||||
onSaved={() => undefined}
|
||||
/>
|
||||
<AssetForm companyId={companyId} items={[]} lookups={lookups} onSaved={() => undefined} />
|
||||
);
|
||||
|
||||
for (const label of ["Nueva plantilla", "Nueva sede"]) {
|
||||
@ -102,7 +96,6 @@ test("El activo ofrece altas relacionadas, ayuda inferior y no encadena una terc
|
||||
"es",
|
||||
<CatalogTemplateForm
|
||||
allowQuickCreate={false}
|
||||
csrfToken="test"
|
||||
items={[]}
|
||||
onSaved={() => undefined}
|
||||
scope={companyId}
|
||||
@ -136,12 +129,12 @@ test("Las fichas leen ambos sentidos y conservan la auditoría de relaciones ret
|
||||
};
|
||||
const source = await render(
|
||||
"es",
|
||||
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />,
|
||||
<AssetRelationships assetId={sourceId} companyId={companyId} />,
|
||||
[link]
|
||||
);
|
||||
const target = await render(
|
||||
"es",
|
||||
<AssetRelationships assetId={targetId} companyId={companyId} csrfToken="test" />,
|
||||
<AssetRelationships assetId={targetId} companyId={companyId} />,
|
||||
[link]
|
||||
);
|
||||
|
||||
@ -151,7 +144,7 @@ test("Las fichas leen ambos sentidos y conservan la auditoría de relaciones ret
|
||||
assert.ok(source.includes("Retirar relación"));
|
||||
const ended = await render(
|
||||
"es",
|
||||
<AssetRelationships assetId={sourceId} companyId={companyId} csrfToken="test" />,
|
||||
<AssetRelationships assetId={sourceId} companyId={companyId} />,
|
||||
[
|
||||
{
|
||||
...link,
|
||||
@ -170,7 +163,7 @@ test("El formulario de tipo de relación presenta ayuda justo antes de sus opera
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await render(
|
||||
language,
|
||||
<CatalogRelationForm csrfToken="test" onSaved={() => undefined} scope={companyId} />
|
||||
<CatalogRelationForm onSaved={() => undefined} scope={companyId} />
|
||||
);
|
||||
|
||||
assert.ok(html.indexOf('name="description"') < html.indexOf("<aside"));
|
||||
@ -187,7 +180,7 @@ test("Puestos de trabajo: listado traducido, alta en cabecera y formulario modal
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await render(
|
||||
language,
|
||||
<WorkstationsPage companyId={companyId} csrfToken="test" lookups={lookups} />
|
||||
<WorkstationsPage companyId={companyId} lookups={lookups} />
|
||||
);
|
||||
|
||||
assert.ok(html.includes(language === "es" ? "Nuevo puesto" : "New workstation"));
|
||||
@ -204,12 +197,7 @@ test("Puestos de trabajo: listado traducido, alta en cabecera y formulario modal
|
||||
test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no ofrece terceras altas", async () => {
|
||||
const html = await render(
|
||||
"es",
|
||||
<WorkstationForm
|
||||
companyId={companyId}
|
||||
csrfToken="test"
|
||||
lookups={lookups}
|
||||
onSaved={() => undefined}
|
||||
/>
|
||||
<WorkstationForm companyId={companyId} lookups={lookups} onSaved={() => undefined} />
|
||||
);
|
||||
|
||||
for (const label of ["Nueva sede", "Nueva persona", "Nuevo activo"]) {
|
||||
@ -222,7 +210,6 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no
|
||||
<AssetForm
|
||||
allowQuickCreate={false}
|
||||
companyId={companyId}
|
||||
csrfToken="test"
|
||||
items={[]}
|
||||
lookups={lookups}
|
||||
onSaved={() => undefined}
|
||||
@ -279,7 +266,6 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace
|
||||
asset={asset}
|
||||
canEdit
|
||||
companyId={companyId}
|
||||
csrfToken="test"
|
||||
lookups={{
|
||||
...lookups,
|
||||
sites: [{ id: siteId, name: "Fábrica" }],
|
||||
|
||||
@ -17,7 +17,6 @@ export type InventoryLookups = {
|
||||
|
||||
export type AssetFormProps = {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
asset?: AssetDTO | undefined;
|
||||
items: readonly CatalogItemDTO[];
|
||||
lookups: InventoryLookups;
|
||||
|
||||
@ -7,7 +7,6 @@ import { AssetForm } from "./asset-form";
|
||||
export function AssetEditorDialog({
|
||||
asset,
|
||||
companyId,
|
||||
csrfToken,
|
||||
items,
|
||||
lookups,
|
||||
onSaved,
|
||||
@ -15,7 +14,6 @@ export function AssetEditorDialog({
|
||||
}: {
|
||||
asset: ComponentProps<typeof AssetForm>["asset"];
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
items: ComponentProps<typeof AssetForm>["items"];
|
||||
lookups: ComponentProps<typeof AssetForm>["lookups"];
|
||||
onSaved: (id: string) => void;
|
||||
@ -39,7 +37,6 @@ export function AssetEditorDialog({
|
||||
<AssetForm
|
||||
asset={asset}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
items={items}
|
||||
lookups={lookups}
|
||||
onSaved={onSaved}
|
||||
|
||||
@ -20,7 +20,7 @@ import type { AssetFormProps } from "../types";
|
||||
import { AssetCharacteristicControl } from "./asset-characteristic-control";
|
||||
|
||||
export function AssetForm(props: AssetFormProps) {
|
||||
const { companyId, csrfToken, asset, items, lookups, allowQuickCreate = true } = props;
|
||||
const { companyId, asset, items, lookups, allowQuickCreate = true } = props;
|
||||
const {
|
||||
creatingTemplate,
|
||||
setCreatingTemplate,
|
||||
@ -125,7 +125,6 @@ export function AssetForm(props: AssetFormProps) {
|
||||
<BrandSelect
|
||||
allowQuickCreate={allowQuickCreate}
|
||||
brandId={values.brandId ?? null}
|
||||
csrfToken={csrfToken}
|
||||
disabled={mutation.isPending}
|
||||
id={`${prefix}-${field.fieldId}`}
|
||||
key={field.fieldId}
|
||||
@ -190,7 +189,6 @@ export function AssetForm(props: AssetFormProps) {
|
||||
</form>
|
||||
{creatingTemplate && (
|
||||
<CatalogTemplateCreateDialog
|
||||
csrfToken={csrfToken}
|
||||
items={items}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
|
||||
@ -5,11 +5,11 @@ import type { BrandsResponseSchema } from "../../../../common/brands";
|
||||
import { brandApi, brandKeys } from "../../shared/api";
|
||||
import type { DeleteBrandDialogProps } from "../types";
|
||||
|
||||
export function useDeleteBrandController({ brand, csrfToken }: DeleteBrandDialogProps) {
|
||||
export function useDeleteBrandController({ brand }: DeleteBrandDialogProps) {
|
||||
const client = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => brandApi.remove(brand.id, csrfToken),
|
||||
mutationFn: () => brandApi.remove(brand.id),
|
||||
onSuccess: async (response) => {
|
||||
client.setQueryData<z.infer<typeof BrandsResponseSchema>>(brandKeys.all, (previous) =>
|
||||
previous
|
||||
|
||||
@ -2,6 +2,5 @@ import type { BrandDTO } from "../../../../common/brands";
|
||||
|
||||
export type DeleteBrandDialogProps = {
|
||||
brand: BrandDTO;
|
||||
csrfToken: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export type BrandCatalogPageProps = {
|
||||
csrfToken: string;
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export type { BrandCatalogPageProps } from "./brands-page-props";
|
||||
@ -14,10 +14,8 @@ import { DeleteBrandDialog } from "../../delete";
|
||||
import { BrandEditorDialog } from "../../shared";
|
||||
import { BrandLogo } from "../../shared/ui";
|
||||
import { useBrandCatalogPageController } from "../controllers";
|
||||
import type { BrandCatalogPageProps } from "../types";
|
||||
|
||||
export function BrandCatalogPage(props: BrandCatalogPageProps) {
|
||||
const { csrfToken } = props;
|
||||
export function BrandCatalogPage() {
|
||||
const { query, edit, setEdit, open, setOpen, deleting, setDeleting, status, setStatus, items } =
|
||||
useBrandCatalogPageController();
|
||||
const { t, i18n } = useTranslation("assets");
|
||||
@ -137,19 +135,12 @@ export function BrandCatalogPage(props: BrandCatalogPageProps) {
|
||||
unavailable={query.isError}
|
||||
/>
|
||||
<BrandEditorDialog
|
||||
csrfToken={csrfToken}
|
||||
onOpenChange={setOpen}
|
||||
open={open}
|
||||
{...(edit ? { brand: edit } : {})}
|
||||
onSaved={() => setOpen(false)}
|
||||
/>
|
||||
{deleting && (
|
||||
<DeleteBrandDialog
|
||||
brand={deleting}
|
||||
csrfToken={csrfToken}
|
||||
onClose={() => setDeleting(undefined)}
|
||||
/>
|
||||
)}
|
||||
{deleting && <DeleteBrandDialog brand={deleting} onClose={() => setDeleting(undefined)} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { BrandInput } from "../../../../common/brands";
|
||||
@ -11,16 +12,11 @@ export class BrandApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function request(
|
||||
path = "",
|
||||
body?: { values: BrandInput } | Record<string, never>,
|
||||
token?: string
|
||||
) {
|
||||
const response = await fetch(`/api/asset-brands${path}`, {
|
||||
async function request(path = "", body?: { values: BrandInput } | Record<string, never>) {
|
||||
const response = await csrfFetch(`/api/asset-brands${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
credentials: "same-origin",
|
||||
headers:
|
||||
body === undefined ? {} : { "content-type": "application/json", "x-csrf-token": token ?? "" },
|
||||
headers: body === undefined ? {} : { "content-type": "application/json" },
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
const data: unknown = await response.json();
|
||||
@ -38,10 +34,10 @@ export const brandApi = {
|
||||
async list() {
|
||||
return BrandsResponseSchema.parse(await request());
|
||||
},
|
||||
async save(values: BrandInput, token: string, id?: string) {
|
||||
return BrandResponseSchema.parse(await request(id ? `/${id}/update` : "", { values }, token));
|
||||
async save(values: BrandInput, id?: string) {
|
||||
return BrandResponseSchema.parse(await request(id ? `/${id}/update` : "", { values }));
|
||||
},
|
||||
async remove(id: string, token: string) {
|
||||
return BrandResponseSchema.parse(await request(`/${id}/delete`, {}, token));
|
||||
async remove(id: string) {
|
||||
return BrandResponseSchema.parse(await request(`/${id}/delete`, {}));
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { configureCsrfTokenReader } from "@metis/core/client";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe } from "@repo/rdx-utils";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
@ -51,7 +52,7 @@ async function render(language: string, node: ReactNode, active = true) {
|
||||
|
||||
test("Marcas: listado común traducido, filtros y alta modal sin formulario junto a la tabla", async () => {
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await render(language, <BrandCatalogPage csrfToken="test" />);
|
||||
const html = await render(language, <BrandCatalogPage />);
|
||||
|
||||
assert.equal(html.includes("<form"), false);
|
||||
assert.ok(html.includes(language === "es" ? "Añadir marca" : "Add brand"));
|
||||
@ -64,24 +65,19 @@ test("Marcas: listado común traducido, filtros y alta modal sin formulario junt
|
||||
}
|
||||
});
|
||||
test("Marcas: nombre de serie protegido y ayuda inferior; alta rápida siempre activa", async () => {
|
||||
const html = await render(
|
||||
"es",
|
||||
<BrandForm brand={brand} csrfToken="test" onSaved={() => undefined} />
|
||||
);
|
||||
const html = await render("es", <BrandForm brand={brand} onSaved={() => undefined} />);
|
||||
|
||||
assert.ok(html.includes('readOnly=""'));
|
||||
assert.ok(html.includes('accept="image/svg+xml,.svg"'));
|
||||
assert.ok(html.includes("Quitar logotipo"));
|
||||
assert.ok(html.indexOf("Una marca identifica") < html.lastIndexOf("Guardar"));
|
||||
const quick = await render(
|
||||
"en",
|
||||
<BrandForm csrfToken="test" onSaved={() => undefined} quickCreate />
|
||||
);
|
||||
const quick = await render("en", <BrandForm onSaved={() => undefined} quickCreate />);
|
||||
|
||||
assert.equal(quick.includes('type="checkbox"'), false);
|
||||
assert.ok(quick.includes("Global scope"));
|
||||
});
|
||||
test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propaga conflictos", async () => {
|
||||
test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propaga conflictos", async (context) => {
|
||||
context.after(configureCsrfTokenReader(() => "a".repeat(64)));
|
||||
const originalFetch = globalThis.fetch;
|
||||
let status = 200;
|
||||
let response: unknown = { brand: { ...brand, builtin: false } };
|
||||
@ -93,7 +89,7 @@ test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propag
|
||||
assert.equal(options?.body, "{}");
|
||||
const headers = new Headers(options?.headers);
|
||||
|
||||
assert.equal(headers.get("x-csrf-token"), "test");
|
||||
assert.equal(headers.get("x-csrf-token"), "a".repeat(64));
|
||||
assert.equal(headers.get("content-type"), "application/json");
|
||||
|
||||
return new Response(JSON.stringify(response), {
|
||||
@ -102,14 +98,14 @@ test("Marcas: eliminar envía CSRF y cuerpo vacío, valida la respuesta y propag
|
||||
});
|
||||
};
|
||||
try {
|
||||
assert.deepEqual(await brandApi.remove(brand.id, "test"), response);
|
||||
assert.deepEqual(await brandApi.remove(brand.id), response);
|
||||
response = { brand: { id: "invalid" } };
|
||||
await assert.rejects(() => brandApi.remove(brand.id, "test"));
|
||||
await assert.rejects(() => brandApi.remove(brand.id));
|
||||
for (const code of ["BRAND_IN_USE", "BUILTIN_PROTECTED", "BRAND_NOT_FOUND"]) {
|
||||
status = code === "BRAND_NOT_FOUND" ? 404 : 409;
|
||||
response = { code };
|
||||
await assert.rejects(
|
||||
() => brandApi.remove(brand.id, "test"),
|
||||
() => brandApi.remove(brand.id),
|
||||
(error: unknown) => error instanceof BrandApiError && error.code === code
|
||||
);
|
||||
}
|
||||
@ -123,7 +119,6 @@ test("Marcas: selector mantiene valores antiguos e inactivos y respeta el límit
|
||||
label: "Marca",
|
||||
brandId: brand.id,
|
||||
legacyName: "LG",
|
||||
csrfToken: "test",
|
||||
disabled: false,
|
||||
required: false,
|
||||
allowQuickCreate: true,
|
||||
@ -183,7 +178,6 @@ test("Marcas: el campo de serie se identifica por referencia de catálogo, no po
|
||||
<AssetForm
|
||||
asset={asset}
|
||||
companyId={asset.companyId}
|
||||
csrfToken="test"
|
||||
items={items}
|
||||
lookups={{
|
||||
sites: [],
|
||||
|
||||
@ -8,7 +8,7 @@ import { BRAND_LOGO_MAX_BYTES, BrandInputSchema, BrandLogoSchema } from "../../.
|
||||
import { brandApi, brandKeys } from "../api";
|
||||
import type { BrandFormProps } from "../types";
|
||||
|
||||
export function useBrandFormController({ csrfToken, brand, onSaved }: BrandFormProps) {
|
||||
export function useBrandFormController({ brand, onSaved }: BrandFormProps) {
|
||||
const mounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
@ -75,7 +75,7 @@ export function useBrandFormController({ csrfToken, brand, onSaved }: BrandFormP
|
||||
};
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: BrandInput) => brandApi.save(values, csrfToken, brand?.id),
|
||||
mutationFn: (values: BrandInput) => brandApi.save(values, brand?.id),
|
||||
onSuccess: async (response) => {
|
||||
// Publicar primero garantiza que el selector pueda mostrar y seleccionar el nuevo valor.
|
||||
client.setQueryData<{
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { BrandDTO } from "../../../../common/brands";
|
||||
|
||||
export type BrandFormProps = {
|
||||
csrfToken: string;
|
||||
brand?: BrandDTO;
|
||||
onSaved: (brand: BrandDTO) => void;
|
||||
quickCreate?: boolean;
|
||||
|
||||
@ -5,7 +5,6 @@ export type BrandSelectProps = {
|
||||
label: string;
|
||||
brandId: string | null;
|
||||
legacyName: string;
|
||||
csrfToken: string;
|
||||
disabled: boolean;
|
||||
required: boolean;
|
||||
allowQuickCreate: boolean;
|
||||
|
||||
@ -11,17 +11,7 @@ import { BrandEditorDialog } from "./brand-editor-dialog";
|
||||
const LEGACY = "legacy";
|
||||
|
||||
export function BrandSelect(props: BrandSelectProps) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
brandId,
|
||||
legacyName,
|
||||
csrfToken,
|
||||
disabled,
|
||||
required,
|
||||
allowQuickCreate,
|
||||
onChange,
|
||||
} = props;
|
||||
const { id, label, brandId, legacyName, disabled, required, allowQuickCreate, onChange } = props;
|
||||
const { query, open, setOpen, brands, legacy, options } = useBrandSelectController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
|
||||
@ -73,7 +63,6 @@ export function BrandSelect(props: BrandSelectProps) {
|
||||
)}
|
||||
{open && (
|
||||
<BrandEditorDialog
|
||||
csrfToken={csrfToken}
|
||||
onOpenChange={setOpen}
|
||||
onSaved={(brand) => {
|
||||
onChange(brand);
|
||||
|
||||
@ -5,12 +5,10 @@ import { CatalogFieldForm } from "../../shared";
|
||||
|
||||
export function CatalogFieldCreateDialog({
|
||||
scope,
|
||||
csrfToken,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
onSaved: (id: string) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
@ -25,7 +23,7 @@ export function CatalogFieldCreateDialog({
|
||||
title={t("newField")}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{t("quickNotice")}</p>
|
||||
<CatalogFieldForm csrfToken={csrfToken} onSaved={onSaved} scope={scope} />
|
||||
<CatalogFieldForm onSaved={onSaved} scope={scope} />
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,12 +5,7 @@ import { useCatalogSave } from "../../../catalog/shared/controllers";
|
||||
import { FieldFormSchema } from "../schemas";
|
||||
import type { CatalogFieldFormProps, FieldFormValues } from "../types";
|
||||
|
||||
export function useCatalogFieldFormController({
|
||||
scope,
|
||||
csrfToken,
|
||||
item,
|
||||
onSaved,
|
||||
}: CatalogFieldFormProps) {
|
||||
export function useCatalogFieldFormController({ scope, item, onSaved }: CatalogFieldFormProps) {
|
||||
const current = item?.values.kind === "field" ? item.values : undefined;
|
||||
const form = useForm<FieldFormValues>({
|
||||
resolver: zodResolver(FieldFormSchema),
|
||||
@ -23,7 +18,7 @@ export function useCatalogFieldFormController({
|
||||
choicesText: current?.choices.join("\n") ?? "",
|
||||
},
|
||||
});
|
||||
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id);
|
||||
const mutation = useCatalogSave(scope, onSaved, item?.id);
|
||||
const type = form.watch("type");
|
||||
const submit = form.handleSubmit(({ choicesText, ...values }) =>
|
||||
mutation.mutate({
|
||||
|
||||
@ -8,7 +8,6 @@ export type FieldFormValues = z.infer<typeof FieldFormSchema>;
|
||||
|
||||
export type CatalogFieldFormProps = {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
item?: CatalogItemDTO | undefined;
|
||||
onSaved: CatalogSaved;
|
||||
};
|
||||
|
||||
@ -5,12 +5,10 @@ import { CatalogRelationForm } from "../../shared";
|
||||
|
||||
export function CatalogRelationCreateDialog({
|
||||
companyId,
|
||||
csrfToken,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
onSaved: (id: string) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
@ -25,7 +23,7 @@ export function CatalogRelationCreateDialog({
|
||||
title={t("newRelation")}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p>
|
||||
<CatalogRelationForm csrfToken={csrfToken} onSaved={onSaved} scope={companyId} />
|
||||
<CatalogRelationForm onSaved={onSaved} scope={companyId} />
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import type { CatalogRelationFormProps } from "../types";
|
||||
|
||||
export function useCatalogRelationFormController({
|
||||
scope,
|
||||
csrfToken,
|
||||
item,
|
||||
onSaved,
|
||||
}: CatalogRelationFormProps) {
|
||||
@ -23,7 +22,7 @@ export function useCatalogRelationFormController({
|
||||
symmetric: false,
|
||||
},
|
||||
});
|
||||
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id);
|
||||
const mutation = useCatalogSave(scope, onSaved, item?.id);
|
||||
const symmetric = form.watch("symmetric");
|
||||
const submit = form.handleSubmit((values) => mutation.mutate(values));
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import type { CatalogSaved } from "../../../catalog/shared/controllers";
|
||||
|
||||
export type CatalogRelationFormProps = {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
item?: CatalogItemDTO | undefined;
|
||||
onSaved: CatalogSaved;
|
||||
};
|
||||
|
||||
@ -6,13 +6,11 @@ import { CatalogTemplateForm } from "../../shared";
|
||||
|
||||
export function CatalogTemplateCreateDialog({
|
||||
scope,
|
||||
csrfToken,
|
||||
items,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
items: ComponentProps<typeof CatalogTemplateForm>["items"];
|
||||
onSaved: (id: string) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -29,13 +27,7 @@ export function CatalogTemplateCreateDialog({
|
||||
wide
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p>
|
||||
<CatalogTemplateForm
|
||||
allowQuickCreate={false}
|
||||
csrfToken={csrfToken}
|
||||
items={items}
|
||||
onSaved={onSaved}
|
||||
scope={scope}
|
||||
/>
|
||||
<CatalogTemplateForm allowQuickCreate={false} items={items} onSaved={onSaved} scope={scope} />
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import type { CatalogTemplateFormProps, TemplateFormValues } from "../types";
|
||||
|
||||
export function useCatalogTemplateFormController({
|
||||
scope,
|
||||
csrfToken,
|
||||
item,
|
||||
copy,
|
||||
items,
|
||||
@ -36,7 +35,7 @@ export function useCatalogTemplateFormController({
|
||||
sections: layout.sections,
|
||||
},
|
||||
});
|
||||
const mutation = useCatalogSave(scope, csrfToken, onSaved, item?.id);
|
||||
const mutation = useCatalogSave(scope, onSaved, item?.id);
|
||||
const selected = form.watch("fields");
|
||||
const fields = items.filter((entry) => entry.values.kind === "field");
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ export type TemplateFormValues = z.infer<typeof TemplateFormSchema>;
|
||||
|
||||
export type CatalogTemplateFormProps = {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
item?: CatalogItemDTO | undefined;
|
||||
copy?: CatalogItemDTO | undefined;
|
||||
items: readonly CatalogItemDTO[];
|
||||
|
||||
@ -17,7 +17,7 @@ import { useCatalogTemplateFormController } from "../controllers";
|
||||
import type { CatalogTemplateFormProps } from "../types";
|
||||
|
||||
export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
|
||||
const { scope, csrfToken, allowQuickCreate = true } = props;
|
||||
const { scope, allowQuickCreate = true } = props;
|
||||
const { creatingField, setCreatingField, form, mutation, selected, fields, add, submit } =
|
||||
useCatalogTemplateFormController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
@ -117,7 +117,6 @@ export function CatalogTemplateForm(props: CatalogTemplateFormProps) {
|
||||
</form>
|
||||
{creatingField && (
|
||||
<CatalogFieldCreateDialog
|
||||
csrfToken={csrfToken}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCreatingField(false);
|
||||
|
||||
@ -7,7 +7,7 @@ import type { CatalogEditor } from "../../shared";
|
||||
import { catalogApi, catalogKeys } from "../../shared/api";
|
||||
import type { AssetCatalogPageProps } from "../types";
|
||||
|
||||
export function useAssetCatalogPageController({ scope, csrfToken }: AssetCatalogPageProps) {
|
||||
export function useAssetCatalogPageController({ scope }: AssetCatalogPageProps) {
|
||||
const client = useQueryClient();
|
||||
const { searchParams, setSearchParams } = useBrowserSearchParams();
|
||||
const requestedTab = searchParams.get("tab");
|
||||
@ -42,7 +42,7 @@ export function useAssetCatalogPageController({ scope, csrfToken }: AssetCatalog
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ item, active }: { item: CatalogItemDTO; active: boolean | null }) =>
|
||||
catalogApi.status(scope, item.id, active, csrfToken),
|
||||
catalogApi.status(scope, item.id, active),
|
||||
onSuccess: async (response) => {
|
||||
await client.invalidateQueries({ queryKey: ["asset-catalog"] });
|
||||
client.setQueryData(catalogKeys.scope(scope), response);
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export type AssetCatalogPageProps = {
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
};
|
||||
|
||||
@ -10,7 +10,7 @@ import type { AssetCatalogPageProps } from "../types";
|
||||
import { CatalogCell } from "./catalog-cell";
|
||||
|
||||
export function AssetCatalogPage(props: AssetCatalogPageProps) {
|
||||
const { scope, csrfToken } = props;
|
||||
const { scope } = props;
|
||||
const {
|
||||
tab,
|
||||
setTab,
|
||||
@ -177,7 +177,6 @@ export function AssetCatalogPage(props: AssetCatalogPageProps) {
|
||||
<CatalogDefinitionDialog item={view} items={items} onClose={() => setViewId(undefined)} />
|
||||
{editor && (
|
||||
<CatalogEditorDialog
|
||||
csrfToken={csrfToken}
|
||||
editor={editor}
|
||||
items={items}
|
||||
onClose={() => setEditor(undefined)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { AssetDefinitionSpec } from "../../../../common";
|
||||
@ -11,14 +12,11 @@ export class CatalogApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path: string, body?: unknown, csrfToken?: string) {
|
||||
const response = await fetch(`/api/asset-catalog/${path}`, {
|
||||
async function request(path: string, body?: unknown) {
|
||||
const response = await csrfFetch(`/api/asset-catalog/${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
credentials: "same-origin",
|
||||
headers:
|
||||
body === undefined
|
||||
? {}
|
||||
: { "content-type": "application/json", "x-csrf-token": csrfToken ?? "" },
|
||||
headers: body === undefined ? {} : { "content-type": "application/json" },
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
const json: unknown = await response.json();
|
||||
@ -36,14 +34,12 @@ export const catalogApi = {
|
||||
async get(scope: string) {
|
||||
return CatalogResponseSchema.parse(await request(scope));
|
||||
},
|
||||
async save(scope: string, values: AssetDefinitionSpec, csrfToken: string, id?: string) {
|
||||
async save(scope: string, values: AssetDefinitionSpec, id?: string) {
|
||||
return SaveDefinitionResponseSchema.parse(
|
||||
await request(`${scope}${id ? `/${id}/update` : ""}`, { values }, csrfToken)
|
||||
await request(`${scope}${id ? `/${id}/update` : ""}`, { values })
|
||||
);
|
||||
},
|
||||
async status(scope: string, id: string, active: boolean | null, csrfToken: string) {
|
||||
return CatalogResponseSchema.parse(
|
||||
await request(`${scope}/${id}/status`, { active }, csrfToken)
|
||||
);
|
||||
async status(scope: string, id: string, active: boolean | null) {
|
||||
return CatalogResponseSchema.parse(await request(`${scope}/${id}/status`, { active }));
|
||||
},
|
||||
};
|
||||
|
||||
@ -48,7 +48,7 @@ test("el catálogo muestra plantillas de serie y acción de alta sin formularios
|
||||
["en", "New template", "Actions"],
|
||||
]) {
|
||||
assert.ok(language && create && actions);
|
||||
const html = await render(language, <AssetCatalogPage csrfToken="test" scope="global" />);
|
||||
const html = await render(language, <AssetCatalogPage scope="global" />);
|
||||
|
||||
assert.ok(html.includes("Portátil"));
|
||||
assert.ok(html.includes(create));
|
||||
@ -58,17 +58,14 @@ test("el catálogo muestra plantillas de serie y acción de alta sin formularios
|
||||
}
|
||||
});
|
||||
test("los formularios tienen ayuda inferior y selector de campo con alta integrada", async () => {
|
||||
const field = await render(
|
||||
"es",
|
||||
<CatalogFieldForm csrfToken="test" onSaved={() => undefined} scope="global" />
|
||||
);
|
||||
const field = await render("es", <CatalogFieldForm onSaved={() => undefined} scope="global" />);
|
||||
|
||||
assert.ok(field.indexOf('name="description"') < field.indexOf("<aside"));
|
||||
assert.ok(field.indexOf("<aside") < field.indexOf('type="submit"'));
|
||||
assert.ok(field.includes("El tipo de dato queda fijo"));
|
||||
const template = await render(
|
||||
"es",
|
||||
<CatalogTemplateForm csrfToken="test" items={[]} onSaved={() => undefined} scope="global" />
|
||||
<CatalogTemplateForm items={[]} onSaved={() => undefined} scope="global" />
|
||||
);
|
||||
|
||||
assert.ok(template.includes('aria-label="Nuevo campo"'));
|
||||
|
||||
@ -6,12 +6,7 @@ import { catalogApi, catalogKeys } from "../api";
|
||||
|
||||
export type CatalogSaved = (id: string, data: SaveDefinitionResponseDTO) => void;
|
||||
|
||||
export function useCatalogSave(
|
||||
scope: string,
|
||||
csrfToken: string,
|
||||
onSaved: CatalogSaved,
|
||||
id?: string
|
||||
) {
|
||||
export function useCatalogSave(scope: string, onSaved: CatalogSaved, id?: string) {
|
||||
const client = useQueryClient();
|
||||
const mounted = useRef(true);
|
||||
|
||||
@ -24,7 +19,7 @@ export function useCatalogSave(
|
||||
}, []);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, csrfToken, id),
|
||||
mutationFn: (values: AssetDefinitionSpec) => catalogApi.save(scope, values, id),
|
||||
onSuccess: async (response) => {
|
||||
await client.invalidateQueries({ queryKey: ["asset-catalog"] });
|
||||
client.setQueryData(catalogKeys.scope(scope), { items: response.items });
|
||||
|
||||
@ -27,13 +27,11 @@ export function CatalogEditorDialog({
|
||||
editor,
|
||||
items,
|
||||
scope,
|
||||
csrfToken,
|
||||
onClose,
|
||||
}: {
|
||||
editor: CatalogEditor;
|
||||
items: readonly CatalogItemDTO[];
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("assets");
|
||||
@ -52,23 +50,12 @@ export function CatalogEditorDialog({
|
||||
wide={editor.kind === "template"}
|
||||
>
|
||||
{editor.kind === "field" ? (
|
||||
<CatalogFieldForm
|
||||
csrfToken={csrfToken}
|
||||
item={editor.item}
|
||||
onSaved={() => onClose()}
|
||||
scope={scope}
|
||||
/>
|
||||
<CatalogFieldForm item={editor.item} onSaved={() => onClose()} scope={scope} />
|
||||
) : editor.kind === "relation" ? (
|
||||
<CatalogRelationForm
|
||||
csrfToken={csrfToken}
|
||||
item={editor.item}
|
||||
onSaved={() => onClose()}
|
||||
scope={scope}
|
||||
/>
|
||||
<CatalogRelationForm item={editor.item} onSaved={() => onClose()} scope={scope} />
|
||||
) : (
|
||||
<CatalogTemplateForm
|
||||
copy={editor.copy}
|
||||
csrfToken={csrfToken}
|
||||
item={editor.item}
|
||||
items={items}
|
||||
onSaved={() => onClose()}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export async function deleteRecord(path: string, token: string) {
|
||||
const response = await fetch(path, {
|
||||
export async function deleteRecord(path: string) {
|
||||
const response = await csrfFetch(path, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "content-type": "application/json", "x-csrf-token": token },
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { InventoryApiError } from "./inventory-api-error";
|
||||
|
||||
export async function inventoryRequest(path: string, body?: unknown, csrfToken?: string) {
|
||||
const response = await fetch(`/api/assets/${path}`, {
|
||||
export async function inventoryRequest(path: string, body?: unknown) {
|
||||
const response = await csrfFetch(`/api/assets/${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
credentials: "same-origin",
|
||||
headers:
|
||||
body === undefined
|
||||
? {}
|
||||
: { "content-type": "application/json", "x-csrf-token": csrfToken ?? "" },
|
||||
headers: body === undefined ? {} : { "content-type": "application/json" },
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
const data: unknown = await response.json();
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { SupportApiError } from "./support-api-error";
|
||||
|
||||
export async function supportRequest(path: string, values?: unknown, token?: string) {
|
||||
const response = await fetch(`/api/asset-support/${path}`, {
|
||||
export async function supportRequest(path: string, values?: unknown) {
|
||||
const response = await csrfFetch(`/api/asset-support/${path}`, {
|
||||
method: values === undefined ? "GET" : "POST",
|
||||
credentials: "same-origin",
|
||||
headers:
|
||||
values === undefined
|
||||
? {}
|
||||
: { "content-type": "application/json", "x-csrf-token": token ?? "" },
|
||||
headers: values === undefined ? {} : { "content-type": "application/json" },
|
||||
...(values === undefined ? {} : { body: JSON.stringify({ values }) }),
|
||||
});
|
||||
const data: unknown = await response.json();
|
||||
|
||||
@ -6,13 +6,11 @@ import { deleteRecord } from "../api/delete-record.api";
|
||||
|
||||
export function DeleteRecordDialog({
|
||||
path,
|
||||
csrfToken,
|
||||
queryKey,
|
||||
onClose,
|
||||
companyId,
|
||||
}: {
|
||||
path: string | undefined;
|
||||
csrfToken: string;
|
||||
queryKey: readonly string[];
|
||||
onClose: () => void;
|
||||
companyId: string;
|
||||
@ -30,7 +28,7 @@ export function DeleteRecordDialog({
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
await deleteRecord(path, csrfToken);
|
||||
await deleteRecord(path);
|
||||
await Promise.all([
|
||||
client.invalidateQueries({ queryKey }),
|
||||
client.invalidateQueries({ queryKey: ["person-processes", companyId] }),
|
||||
|
||||
@ -2,6 +2,5 @@ export type AssetSupportProps = {
|
||||
compact?: boolean;
|
||||
companyId: string;
|
||||
assetId: string;
|
||||
csrfToken: string;
|
||||
retired?: boolean;
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@ import { useAssetSupportController } from "../controllers";
|
||||
import type { AssetSupportProps } from "../types";
|
||||
|
||||
export function AssetSupport(props: AssetSupportProps) {
|
||||
const { companyId, assetId, csrfToken, retired = false, compact = false } = props;
|
||||
const { companyId, assetId, retired = false, compact = false } = props;
|
||||
const {
|
||||
query,
|
||||
open,
|
||||
@ -110,7 +110,6 @@ export function AssetSupport(props: AssetSupportProps) {
|
||||
)}
|
||||
<DeleteRecordDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
onClose={() => setDeleteId(undefined)}
|
||||
path={
|
||||
deleteId ? `/api/asset-support/${companyId}/${assetId}/${deleteId}/delete` : undefined
|
||||
@ -120,14 +119,13 @@ export function AssetSupport(props: AssetSupportProps) {
|
||||
<SupportConnectionEditorDialog
|
||||
assetId={assetId}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
edit={edit}
|
||||
onOpenChange={setOpen}
|
||||
onSaved={() => setOpen(false)}
|
||||
open={open}
|
||||
tools={tools}
|
||||
/>
|
||||
<SupportToolsDialog csrfToken={csrfToken} onOpenChange={setToolsOpen} open={toolsOpen} />
|
||||
<SupportToolsDialog onOpenChange={setToolsOpen} open={toolsOpen} />
|
||||
<SupportHistoryDialog
|
||||
history={history}
|
||||
onOpenChange={(value) => {
|
||||
|
||||
@ -6,10 +6,9 @@ export async function saveSupportConnection(
|
||||
companyId: string,
|
||||
assetId: string,
|
||||
values: SupportInput,
|
||||
token: string,
|
||||
id?: string
|
||||
) {
|
||||
return SupportResponseSchema.parse(
|
||||
await supportRequest(`${companyId}/${assetId}${id ? `/${id}/update` : ""}`, values, token)
|
||||
await supportRequest(`${companyId}/${assetId}${id ? `/${id}/update` : ""}`, values)
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import type { SupportFormProps } from "../types";
|
||||
export function useSupportFormController({
|
||||
companyId,
|
||||
assetId,
|
||||
csrfToken,
|
||||
connection,
|
||||
onSaved,
|
||||
}: SupportFormProps) {
|
||||
@ -32,7 +31,7 @@ export function useSupportFormController({
|
||||
const values = form.watch();
|
||||
const mutation = useMutation({
|
||||
mutationFn: (input: SupportInput) =>
|
||||
saveSupportConnection(companyId, assetId, input, csrfToken, connection?.id),
|
||||
saveSupportConnection(companyId, assetId, input, connection?.id),
|
||||
onSuccess: async () => {
|
||||
await client.invalidateQueries({ queryKey: supportKeys.asset(companyId, assetId) });
|
||||
onSaved();
|
||||
|
||||
@ -46,7 +46,6 @@ test("asistencia: formulario con alta rápida y ayuda antes de guardar", async (
|
||||
<SupportForm
|
||||
assetId={UniqueID.generateNewID().toPrimitive()}
|
||||
companyId={UniqueID.generateNewID().toPrimitive()}
|
||||
csrfToken="test"
|
||||
onSaved={() => undefined}
|
||||
tools={[tool]}
|
||||
/>
|
||||
@ -81,9 +80,7 @@ test("asistencia: traducciones y resumen de método", async () => {
|
||||
}
|
||||
});
|
||||
test("herramienta: nombre de serie protegido y ayuda inferior", async () => {
|
||||
const html = await render(
|
||||
<SupportToolForm csrfToken="test" onSaved={() => undefined} tool={tool} />
|
||||
);
|
||||
const html = await render(<SupportToolForm onSaved={() => undefined} tool={tool} />);
|
||||
|
||||
assert.ok(html.includes("readOnly"));
|
||||
assert.ok(html.indexOf("¿Para qué sirve una herramienta?") < html.indexOf("Guardar"));
|
||||
|
||||
@ -3,7 +3,6 @@ import type { SupportConnectionDTO, SupportToolDTO } from "../../../../common/su
|
||||
export type SupportFormProps = {
|
||||
companyId: string;
|
||||
assetId: string;
|
||||
csrfToken: string;
|
||||
tools: readonly SupportToolDTO[];
|
||||
connection?: SupportConnectionDTO;
|
||||
onSaved: () => void;
|
||||
|
||||
@ -9,7 +9,6 @@ export function SupportConnectionEditorDialog({
|
||||
edit,
|
||||
companyId,
|
||||
assetId,
|
||||
csrfToken,
|
||||
tools,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
@ -18,7 +17,6 @@ export function SupportConnectionEditorDialog({
|
||||
edit: ComponentProps<typeof SupportForm>["connection"];
|
||||
companyId: string;
|
||||
assetId: string;
|
||||
csrfToken: string;
|
||||
tools: ComponentProps<typeof SupportForm>["tools"];
|
||||
onSaved: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -36,7 +34,6 @@ export function SupportConnectionEditorDialog({
|
||||
<SupportForm
|
||||
assetId={assetId}
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
key={edit?.id ?? "new"}
|
||||
tools={tools}
|
||||
{...(edit ? { connection: edit } : {})}
|
||||
|
||||
@ -15,7 +15,7 @@ import type { SupportFormProps } from "../types";
|
||||
import { SupportHelp } from "./support-help";
|
||||
|
||||
export function SupportForm(props: SupportFormProps) {
|
||||
const { csrfToken, tools, connection } = props;
|
||||
const { tools, connection } = props;
|
||||
const { quick, setQuick, form, values, mutation, submit } = useSupportFormController(props);
|
||||
const { t } = useTranslation("assets");
|
||||
const prefix = useId();
|
||||
@ -104,7 +104,6 @@ export function SupportForm(props: SupportFormProps) {
|
||||
</FormActions>
|
||||
</form>
|
||||
<SupportToolEditorDialog
|
||||
csrfToken={csrfToken}
|
||||
edit={undefined}
|
||||
onOpenChange={setQuick}
|
||||
onSaved={(id) => {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export type { SupportToolsProps } from "./support-tools-props";
|
||||
@ -1,3 +0,0 @@
|
||||
export type SupportToolsProps = {
|
||||
csrfToken: string;
|
||||
};
|
||||
@ -5,11 +5,9 @@ import { SupportTools } from "./support-tools";
|
||||
|
||||
export function SupportToolsDialog({
|
||||
open,
|
||||
csrfToken,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
csrfToken: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("assets");
|
||||
@ -21,7 +19,7 @@ export function SupportToolsDialog({
|
||||
open={open}
|
||||
title={t("support.tools")}
|
||||
>
|
||||
<SupportTools csrfToken={csrfToken} />
|
||||
<SupportTools />
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -10,10 +10,8 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { SupportToolEditorDialog } from "../../shared";
|
||||
import { useSupportToolsController } from "../controllers";
|
||||
import type { SupportToolsProps } from "../types";
|
||||
|
||||
export function SupportTools(props: SupportToolsProps) {
|
||||
const { csrfToken } = props;
|
||||
export function SupportTools() {
|
||||
const { query, edit, setEdit, open, setOpen } = useSupportToolsController();
|
||||
const { t, i18n } = useTranslation("assets");
|
||||
|
||||
@ -100,7 +98,6 @@ export function SupportTools(props: SupportToolsProps) {
|
||||
unavailable={query.isError}
|
||||
/>
|
||||
<SupportToolEditorDialog
|
||||
csrfToken={csrfToken}
|
||||
edit={edit}
|
||||
onOpenChange={setOpen}
|
||||
onSaved={() => setOpen(false)}
|
||||
|
||||
@ -2,8 +2,8 @@ import type { SupportToolInput } from "../../../../common/support";
|
||||
import { SupportToolResponseSchema } from "../../../../common/support";
|
||||
import { supportRequest } from "../../../shared/api/support-request";
|
||||
|
||||
export async function saveSupportTool(values: SupportToolInput, token: string, id?: string) {
|
||||
export async function saveSupportTool(values: SupportToolInput, id?: string) {
|
||||
return SupportToolResponseSchema.parse(
|
||||
await supportRequest(`tools${id ? `/${id}/update` : ""}`, values, token)
|
||||
await supportRequest(`tools${id ? `/${id}/update` : ""}`, values)
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,14 +8,14 @@ import { supportKeys } from "../../../shared/queries";
|
||||
import { saveSupportTool } from "../api";
|
||||
import type { SupportToolFormProps } from "../types";
|
||||
|
||||
export function useSupportToolFormController({ csrfToken, tool, onSaved }: SupportToolFormProps) {
|
||||
export function useSupportToolFormController({ tool, onSaved }: SupportToolFormProps) {
|
||||
const client = useQueryClient();
|
||||
const form = useForm<SupportToolInput>({
|
||||
resolver: zodResolver(SupportToolInputSchema),
|
||||
defaultValues: tool ? { name: tool.name, active: tool.active } : { name: "", active: true },
|
||||
});
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: SupportToolInput) => saveSupportTool(values, csrfToken, tool?.id),
|
||||
mutationFn: (values: SupportToolInput) => saveSupportTool(values, tool?.id),
|
||||
onSuccess: async (response) => {
|
||||
await Promise.all([
|
||||
client.invalidateQueries({ queryKey: supportKeys.tools }),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { SupportToolDTO } from "../../../../common/support";
|
||||
|
||||
export type SupportToolFormProps = {
|
||||
csrfToken: string;
|
||||
tool?: SupportToolDTO;
|
||||
onSaved: (id: string) => void;
|
||||
};
|
||||
|
||||
@ -7,13 +7,11 @@ import { SupportToolForm } from "./support-tool-form";
|
||||
export function SupportToolEditorDialog({
|
||||
open,
|
||||
edit,
|
||||
csrfToken,
|
||||
onSaved,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
edit: ComponentProps<typeof SupportToolForm>["tool"];
|
||||
csrfToken: string;
|
||||
onSaved: ComponentProps<typeof SupportToolForm>["onSaved"];
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
@ -28,7 +26,6 @@ export function SupportToolEditorDialog({
|
||||
title={t(edit ? "support.editTool" : "support.newTool")}
|
||||
>
|
||||
<SupportToolForm
|
||||
csrfToken={csrfToken}
|
||||
key={edit?.id ?? "new"}
|
||||
{...(edit ? { tool: edit } : {})}
|
||||
onSaved={onSaved}
|
||||
|
||||
@ -2,6 +2,5 @@ import type { InventoryLookups } from "../../../assets/shared/types";
|
||||
|
||||
export type WorkstationsPageProps = {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
lookups: InventoryLookups;
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ import { useWorkstationsPageController } from "../controllers";
|
||||
import type { WorkstationsPageProps } from "../types";
|
||||
|
||||
export function WorkstationsPage(props: WorkstationsPageProps) {
|
||||
const { companyId, csrfToken, lookups } = props;
|
||||
const { companyId, lookups } = props;
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
@ -152,7 +152,6 @@ export function WorkstationsPage(props: WorkstationsPageProps) {
|
||||
{editor && (
|
||||
<WorkstationEditorDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
lookups={lookups}
|
||||
onClose={() => setEditor(undefined)}
|
||||
workstation={edited}
|
||||
|
||||
@ -2,17 +2,8 @@ import type { WorkstationInput } from "../../../../common/workstations";
|
||||
import { WorkstationResponseSchema } from "../../../../common/workstations";
|
||||
import { inventoryRequest } from "../../../shared/api/inventory-request";
|
||||
|
||||
export async function saveWorkstation(
|
||||
companyId: string,
|
||||
values: WorkstationInput,
|
||||
token: string,
|
||||
id?: string
|
||||
) {
|
||||
export async function saveWorkstation(companyId: string, values: WorkstationInput, id?: string) {
|
||||
return WorkstationResponseSchema.parse(
|
||||
await inventoryRequest(
|
||||
`${companyId}/workstations${id ? `/${id}/update` : ""}`,
|
||||
{ values },
|
||||
token
|
||||
)
|
||||
await inventoryRequest(`${companyId}/workstations${id ? `/${id}/update` : ""}`, { values })
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import type { WorkstationFormProps } from "../types";
|
||||
|
||||
export function useWorkstationFormController({
|
||||
companyId,
|
||||
csrfToken,
|
||||
workstation,
|
||||
onSaved,
|
||||
}: WorkstationFormProps) {
|
||||
@ -76,8 +75,7 @@ export function useWorkstationFormController({
|
||||
};
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (input: WorkstationInput) =>
|
||||
saveWorkstation(companyId, input, csrfToken, workstation?.id),
|
||||
mutationFn: (input: WorkstationInput) => saveWorkstation(companyId, input, workstation?.id),
|
||||
onSuccess: async (response) => {
|
||||
await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) });
|
||||
if (mounted.current) {
|
||||
|
||||
@ -3,7 +3,6 @@ import type { InventoryLookups } from "../../../assets/shared/types";
|
||||
|
||||
export type WorkstationFormProps = {
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
lookups: InventoryLookups;
|
||||
workstation?: WorkstationDTO | undefined;
|
||||
onSaved: (id: string) => void;
|
||||
|
||||
@ -7,13 +7,11 @@ import { WorkstationForm } from "./workstation-form";
|
||||
export function WorkstationEditorDialog({
|
||||
workstation,
|
||||
companyId,
|
||||
csrfToken,
|
||||
lookups,
|
||||
onClose,
|
||||
}: {
|
||||
workstation: ComponentProps<typeof WorkstationForm>["workstation"];
|
||||
companyId: string;
|
||||
csrfToken: string;
|
||||
lookups: ComponentProps<typeof WorkstationForm>["lookups"];
|
||||
onClose: () => void;
|
||||
}) {
|
||||
@ -34,7 +32,6 @@ export function WorkstationEditorDialog({
|
||||
>
|
||||
<WorkstationForm
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
lookups={lookups}
|
||||
onSaved={() => onClose()}
|
||||
workstation={workstation}
|
||||
|
||||
@ -14,7 +14,7 @@ import { useWorkstationFormController } from "../controllers";
|
||||
import type { WorkstationFormProps } from "../types";
|
||||
|
||||
export function WorkstationForm(props: WorkstationFormProps) {
|
||||
const { companyId, csrfToken, lookups, workstation } = props;
|
||||
const { companyId, lookups, workstation } = props;
|
||||
const {
|
||||
form,
|
||||
values,
|
||||
@ -193,7 +193,6 @@ export function WorkstationForm(props: WorkstationFormProps) {
|
||||
{quick && catalog.data && (
|
||||
<WorkstationAssetCreateDialog
|
||||
companyId={companyId}
|
||||
csrfToken={csrfToken}
|
||||
items={catalog.data.items}
|
||||
lookups={lookups}
|
||||
onOpenChange={setQuick}
|
||||
|
||||
@ -11,7 +11,6 @@ import type { QuickCreateDialogProps } from "../types";
|
||||
export function useQuickCreateDialogController({
|
||||
target,
|
||||
scope,
|
||||
csrfToken,
|
||||
onCreated,
|
||||
items,
|
||||
onClose,
|
||||
@ -42,7 +41,7 @@ export function useQuickCreateDialogController({
|
||||
? { kind: "system", name, active: true }
|
||||
: { kind: "environment", name, active: true, systemId: target.systemId }
|
||||
);
|
||||
const result = await saveAccess(scope, values, undefined, csrfToken);
|
||||
const result = await saveAccess(scope, values, undefined);
|
||||
const created = result.items.find(
|
||||
(item) =>
|
||||
item.companyId === (scope === "global" ? null : scope) && matches(item.values, values)
|
||||
|
||||
@ -9,7 +9,6 @@ export type AccessQuickEditorProps = {
|
||||
scope: string;
|
||||
data: AccessResponseDTO;
|
||||
people: readonly PersonDTO[];
|
||||
csrfToken: string;
|
||||
onCreated: (id?: string, values?: AccessFieldsDTO) => void;
|
||||
onClose: () => void;
|
||||
nestedAccessForm: ComponentType<AccessFormProps>;
|
||||
|
||||
@ -13,7 +13,6 @@ export type QuickCreateTarget =
|
||||
export type QuickCreateDialogProps = {
|
||||
target: QuickCreateTarget;
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
onClose: () => void;
|
||||
onCreated: (id: string) => void;
|
||||
items: readonly AccessItemDTO[];
|
||||
|
||||
@ -9,14 +9,13 @@ import type { AccessQuickEditorProps } from "../types";
|
||||
import { QuickCreateDialog } from "./quick-create-dialog";
|
||||
|
||||
export function AccessQuickEditor(props: AccessQuickEditorProps) {
|
||||
const { plan, scope, data, people, csrfToken, nestedAccessForm: NestedAccessForm } = props;
|
||||
const { plan, scope, data, people, nestedAccessForm: NestedAccessForm } = props;
|
||||
const { close, created, globalQuery } = useAccessQuickEditorController(props);
|
||||
const { t } = useTranslation("access");
|
||||
|
||||
if (plan.type === "simple") {
|
||||
return (
|
||||
<QuickCreateDialog
|
||||
csrfToken={csrfToken}
|
||||
items={data.items}
|
||||
onClose={close}
|
||||
onCreated={created}
|
||||
@ -44,7 +43,7 @@ export function AccessQuickEditor(props: AccessQuickEditorProps) {
|
||||
{plan.type === "person" ? (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">{t("quickCreateNotice")}</p>
|
||||
<PersonForm companyId={scope} csrfToken={csrfToken} onSaved={created} requireActive />
|
||||
<PersonForm companyId={scope} onSaved={created} requireActive />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -54,7 +53,6 @@ export function AccessQuickEditor(props: AccessQuickEditorProps) {
|
||||
{childData ? (
|
||||
<NestedAccessForm
|
||||
allowQuickCreate={false}
|
||||
csrfToken={csrfToken}
|
||||
data={childData}
|
||||
initialValues={plan.defaults}
|
||||
kind={plan.kind}
|
||||
|
||||
@ -3,5 +3,4 @@ import type { PersonDTO } from "../../../../common";
|
||||
export type AccessWorkspacePageProps = {
|
||||
scope: string;
|
||||
people?: readonly PersonDTO[];
|
||||
csrfToken: string;
|
||||
};
|
||||
|
||||
@ -17,7 +17,7 @@ function errorCode(error: Error) {
|
||||
}
|
||||
|
||||
export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
|
||||
const { scope, people = [], csrfToken } = props;
|
||||
const { scope, people = [] } = props;
|
||||
const {
|
||||
query,
|
||||
kind,
|
||||
@ -169,7 +169,6 @@ export function AccessWorkspacePage(props: AccessWorkspacePageProps) {
|
||||
/>
|
||||
<AccessEditorDialog
|
||||
complex={complex}
|
||||
csrfToken={csrfToken}
|
||||
data={data}
|
||||
editor={editor}
|
||||
kind={kind}
|
||||
|
||||
@ -32,7 +32,7 @@ test("los catálogos presentan alta mediante acción y no un formulario junto al
|
||||
const markup = renderToStaticMarkup(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryClientProvider client={client}>
|
||||
<AccessWorkspacePage csrfToken="test" scope="global" />
|
||||
<AccessWorkspacePage scope="global" />
|
||||
</QueryClientProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
@ -51,7 +51,6 @@ test("cuentas ofrecen caducidad opcional y campos sin contraseña", async () =>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryClientProvider client={client}>
|
||||
<AccessForm
|
||||
csrfToken="test"
|
||||
data={{ items: [], reviews: [], coverage: [] }}
|
||||
kind="account"
|
||||
onSaved={() => undefined}
|
||||
@ -141,7 +140,6 @@ test("los ejemplos del sistema orientan sin rellenar datos del alta", async () =
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<QueryClientProvider client={client}>
|
||||
<AccessForm
|
||||
csrfToken="test"
|
||||
data={{ items: [], reviews: [], coverage: [] }}
|
||||
kind="system"
|
||||
onSaved={() => undefined}
|
||||
@ -273,7 +271,6 @@ test("los formularios ofrecen altas relacionadas y el segundo modal evita altas
|
||||
<QueryClientProvider client={client}>
|
||||
<AccessForm
|
||||
allowQuickCreate={allowQuickCreate}
|
||||
csrfToken="test"
|
||||
data={{ items: [], reviews: [], coverage: [] }}
|
||||
kind={kind}
|
||||
onSaved={() => undefined}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { AccessFieldsDTO } from "../../../../common";
|
||||
@ -5,16 +6,11 @@ import { AccessResponseSchema } from "../../../../common";
|
||||
|
||||
import { AccessApiError } from "./access-api-error";
|
||||
|
||||
export async function accessRequest(
|
||||
scope: string,
|
||||
values?: AccessFieldsDTO,
|
||||
id?: string,
|
||||
token?: string
|
||||
) {
|
||||
const response = await fetch(`/api/access/${scope}${id ? `/${id}/update` : ""}`, {
|
||||
export async function accessRequest(scope: string, values?: AccessFieldsDTO, id?: string) {
|
||||
const response = await csrfFetch(`/api/access/${scope}${id ? `/${id}/update` : ""}`, {
|
||||
method: values ? "POST" : "GET",
|
||||
credentials: "same-origin",
|
||||
headers: values ? { "content-type": "application/json", "x-csrf-token": token ?? "" } : {},
|
||||
headers: values ? { "content-type": "application/json" } : {},
|
||||
...(values ? { body: JSON.stringify({ values }) } : {}),
|
||||
});
|
||||
const json: unknown = await response.json();
|
||||
|
||||
@ -2,11 +2,6 @@ import type { AccessFieldsDTO } from "../../../../common";
|
||||
|
||||
import { accessRequest } from "./access-request";
|
||||
|
||||
export function saveAccess(
|
||||
scope: string,
|
||||
values: AccessFieldsDTO,
|
||||
id: string | undefined,
|
||||
token: string
|
||||
) {
|
||||
return accessRequest(scope, values, id, token);
|
||||
export function saveAccess(scope: string, values: AccessFieldsDTO, id: string | undefined) {
|
||||
return accessRequest(scope, values, id);
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ export function useAccessFormFieldsController({
|
||||
item,
|
||||
data,
|
||||
people,
|
||||
csrfToken,
|
||||
onSaved,
|
||||
allowQuickCreate = true,
|
||||
initialValues = {},
|
||||
@ -56,7 +55,7 @@ export function useAccessFormFieldsController({
|
||||
throw new AccessApiError("INVALID_REQUEST");
|
||||
}
|
||||
|
||||
return saveAccess(scope, parsed.data, item?.id, csrfToken);
|
||||
return saveAccess(scope, parsed.data, item?.id);
|
||||
},
|
||||
onSuccess: async (result, submitted) => {
|
||||
const savedId =
|
||||
|
||||
@ -13,7 +13,6 @@ export type AccessFormProps = {
|
||||
item?: AccessItemDTO | undefined;
|
||||
data: AccessResponseDTO;
|
||||
people: readonly PersonDTO[];
|
||||
csrfToken: string;
|
||||
onSaved: (id?: string, values?: AccessFieldsDTO) => void;
|
||||
allowQuickCreate?: boolean;
|
||||
initialValues?: AccessFormValues;
|
||||
|
||||
@ -12,7 +12,6 @@ export function AccessEditorDialog({
|
||||
selected,
|
||||
kind,
|
||||
scope,
|
||||
csrfToken,
|
||||
data,
|
||||
people,
|
||||
complex,
|
||||
@ -23,7 +22,6 @@ export function AccessEditorDialog({
|
||||
selected: ComponentProps<typeof AccessForm>["item"];
|
||||
kind: AccessKind;
|
||||
scope: string;
|
||||
csrfToken: string;
|
||||
data: ComponentProps<typeof AccessForm>["data"];
|
||||
people: ComponentProps<typeof AccessForm>["people"];
|
||||
complex: boolean;
|
||||
@ -42,7 +40,6 @@ export function AccessEditorDialog({
|
||||
wide={complex}
|
||||
>
|
||||
<AccessForm
|
||||
csrfToken={csrfToken}
|
||||
data={data}
|
||||
item={selected}
|
||||
key={editor}
|
||||
|
||||
@ -17,7 +17,7 @@ export function AccessFormFields(
|
||||
nestedAccessForm: ComponentType<AccessFormProps>;
|
||||
}
|
||||
) {
|
||||
const { scope, kind, item, data, people, csrfToken, lockedFields = [], nestedAccessForm } = props;
|
||||
const { scope, kind, item, data, people, lockedFields = [], nestedAccessForm } = props;
|
||||
const {
|
||||
validation,
|
||||
quickCreate,
|
||||
@ -94,7 +94,6 @@ export function AccessFormFields(
|
||||
</form>
|
||||
{quickCreate && (
|
||||
<AccessQuickEditor
|
||||
csrfToken={csrfToken}
|
||||
data={data}
|
||||
nestedAccessForm={nestedAccessForm}
|
||||
onClose={() => setQuickCreate(undefined)}
|
||||
|
||||
@ -12,7 +12,6 @@ export function AccessForm(props: AccessFormProps) {
|
||||
<ResourceEditor
|
||||
allowQuickCreate={props.allowQuickCreate ?? true}
|
||||
companyId={props.scope}
|
||||
csrfToken={props.csrfToken}
|
||||
initialAccess={
|
||||
typeof type === "string" && type
|
||||
? { resourceTypeId: type, systemId: null, environmentId: null }
|
||||
|
||||
@ -8,13 +8,11 @@ import { WorkProfileForm } from "../../../work-profiles/shared";
|
||||
export function ApplicationQuickEditor({
|
||||
kind,
|
||||
company,
|
||||
csrfToken,
|
||||
onClose,
|
||||
onSaved,
|
||||
}: {
|
||||
kind: "profile" | "application";
|
||||
company: CompanyDTO;
|
||||
csrfToken: string;
|
||||
onClose: () => void;
|
||||
onSaved: (field: "profileId" | "resourceId", id: string) => void;
|
||||
}) {
|
||||
@ -37,14 +35,12 @@ export function ApplicationQuickEditor({
|
||||
<WorkProfileForm
|
||||
allowQuickCreate={false}
|
||||
company={company}
|
||||
csrfToken={csrfToken}
|
||||
onSaved={(id) => onSaved("profileId", id)}
|
||||
/>
|
||||
) : (
|
||||
<ResourceEditor
|
||||
allowQuickCreate={false}
|
||||
companyId={company.id}
|
||||
csrfToken={csrfToken}
|
||||
initialKind="application"
|
||||
onSaved={(id) => onSaved("resourceId", id)}
|
||||
/>
|
||||
|
||||
@ -2,6 +2,5 @@ import type { CompanyDTO } from "../../../../common";
|
||||
|
||||
export type ApplicationConfigurationsPageProps = {
|
||||
company: CompanyDTO;
|
||||
csrfToken: string;
|
||||
initialProfileId?: string;
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ import { useApplicationConfigurationsPageController } from "../controllers";
|
||||
import type { ApplicationConfigurationsPageProps } from "../types";
|
||||
|
||||
export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPageProps) {
|
||||
const { company, csrfToken, initialProfileId } = props;
|
||||
const { company, initialProfileId } = props;
|
||||
const {
|
||||
editor,
|
||||
setEditor,
|
||||
@ -42,7 +42,6 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa
|
||||
<div className="space-y-5">
|
||||
<DeleteRecordDialog
|
||||
companyId={company.id}
|
||||
csrfToken={csrfToken}
|
||||
onClose={() => setDeleteId(undefined)}
|
||||
path={
|
||||
deleteId ? `/api/application-configurations/${company.id}/${deleteId}/delete` : undefined
|
||||
@ -170,7 +169,6 @@ export function ApplicationConfigurationsPage(props: ApplicationConfigurationsPa
|
||||
<ApplicationConfigurationEditorDialog
|
||||
company={company}
|
||||
configuration={edited}
|
||||
csrfToken={csrfToken}
|
||||
initialProfileId={initialProfileId ?? ""}
|
||||
onClose={() => setEditor(undefined)}
|
||||
/>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { csrfFetch } from "@metis/core/client";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { ApplicationConfigurationInput } from "../../../../common/application-configurations";
|
||||
@ -14,18 +15,13 @@ export class ApplicationApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function request(
|
||||
companyId: string,
|
||||
values?: ApplicationConfigurationInput,
|
||||
token?: string,
|
||||
id?: string
|
||||
) {
|
||||
const response = await fetch(
|
||||
async function request(companyId: string, values?: ApplicationConfigurationInput, id?: string) {
|
||||
const response = await csrfFetch(
|
||||
`/api/application-configurations/${companyId}${id ? `/${id}/update` : ""}`,
|
||||
{
|
||||
method: values ? "POST" : "GET",
|
||||
credentials: "same-origin",
|
||||
headers: values ? { "content-type": "application/json", "x-csrf-token": token ?? "" } : {},
|
||||
headers: values ? { "content-type": "application/json" } : {},
|
||||
...(values ? { body: JSON.stringify({ values }) } : {}),
|
||||
}
|
||||
);
|
||||
@ -44,9 +40,7 @@ export const applicationApi = {
|
||||
async get(companyId: string) {
|
||||
return ApplicationConfigurationsResponseSchema.parse(await request(companyId));
|
||||
},
|
||||
async save(companyId: string, values: ApplicationConfigurationInput, token: string, id?: string) {
|
||||
return ApplicationConfigurationResponseSchema.parse(
|
||||
await request(companyId, values, token, id)
|
||||
);
|
||||
async save(companyId: string, values: ApplicationConfigurationInput, id?: string) {
|
||||
return ApplicationConfigurationResponseSchema.parse(await request(companyId, values, id));
|
||||
},
|
||||
};
|
||||
|
||||
@ -65,7 +65,7 @@ test("aplicaciones: listado sin formularios, alta en cabecera y traducciones", a
|
||||
]) {
|
||||
const html = await render(
|
||||
language ?? "es",
|
||||
<ApplicationConfigurationsPage company={company} csrfToken="test" />
|
||||
<ApplicationConfigurationsPage company={company} />
|
||||
);
|
||||
|
||||
assert.ok(html.includes(title ?? ""));
|
||||
@ -78,7 +78,7 @@ test("aplicaciones: selectores con alta rápida y ayuda inferior antes de guarda
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await render(
|
||||
language,
|
||||
<ApplicationConfigurationForm company={company} csrfToken="test" onSaved={() => undefined} />
|
||||
<ApplicationConfigurationForm company={company} onSaved={() => undefined} />
|
||||
);
|
||||
const text = language === "es" ? companiesSpanish : companiesEnglish;
|
||||
|
||||
|
||||
@ -27,7 +27,6 @@ function applicationStates(
|
||||
|
||||
export function useApplicationConfigurationFormController({
|
||||
company,
|
||||
csrfToken,
|
||||
configuration,
|
||||
initialProfileId = "",
|
||||
onSaved,
|
||||
@ -65,7 +64,7 @@ export function useApplicationConfigurationFormController({
|
||||
const states = applicationStates(configuration);
|
||||
const mutation = useMutation({
|
||||
mutationFn: (input: ApplicationConfigurationInput) =>
|
||||
applicationApi.save(company.id, input, csrfToken, configuration?.id),
|
||||
applicationApi.save(company.id, input, configuration?.id),
|
||||
onSuccess: async () => {
|
||||
await client.invalidateQueries({ queryKey: applicationKeys.company(company.id) });
|
||||
if (mounted.current) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user