feat: add technical logs and implement infrastructure item retrieval
- Created node-check.log to capture Node.js syntax errors. - Added proxy.log for TLS termination diagnostics. - Introduced rdx-ui-types.log and resources-types.log for TypeScript compilation logs. - Implemented resources.log to log test results for various resource-related tests. - Added table.log to log column restoration and preference isolation tests. - Created types.log to log type checking results across multiple packages. - Added web-types.log for TypeScript compilation logs specific to web components. - Introduced traceability.json to track changes in recorded sources and their states. - Implemented infrastructure-items.ts to retrieve infrastructure items with roles and interfaces. - Added company-people-navigation.tsx for navigation in company detail views.
This commit is contained in:
parent
6992a4ac74
commit
0a5fb28263
@ -0,0 +1,14 @@
|
||||
{
|
||||
"apps/server/dist/index.js": "391a1d3f4ae0d5f4231ac487931a93c81b0cc0eb221f33f04ce785983a925507",
|
||||
"apps/server/dist/index.js.map": "bddd81d81d134df7ef35ab4da1dca82d61f0a3bbd59bbc5751a08527affcf315",
|
||||
"apps/web/dist/assets/geist-mono-latin-ext-variable-Bwz-egvJ.woff2": "1a189eb997c3e2ece68373e387afaec9e8617424186c4b1ab3cff7c54ba6223b",
|
||||
"apps/web/dist/assets/geist-mono-latin-variable-XN7g48iV.woff2": "684ad5b531f81d43c1e8c7038262d5db7cdc1f68006e04d6c7769efa8d33c8cc",
|
||||
"apps/web/dist/assets/index-DsAXQkUa.js": "9e9d770b82597b1a7f8c2aa812d9874aa3e84f8a35ddba9400a941b6e1a73f5b",
|
||||
"apps/web/dist/assets/index-ZFzBcHiV.css": "c8a8acc5a0ef0f1891efb7fcab2ca6fa100458ad27df938dd374aee081e7200b",
|
||||
"apps/web/dist/assets/inter-variable-DiVDrmQJ.woff2": "693b77d4f32ee9b8bfc995589b5fad5e99adf2832738661f5402f9978429a8e3",
|
||||
"apps/web/dist/assets/roboto-serif-latin-ext-variable-B9BEn9Tg.woff2": "90626de112742b75e72b27865650de397eccf6845b22b577b0283b9cbfda83fc",
|
||||
"apps/web/dist/assets/roboto-serif-latin-variable-CusprKnR.woff2": "a32deb82e55d4bcb083e09dbb4da3198011ce0e9f919877179e8c2bca23a9042",
|
||||
"apps/web/dist/assets/workplace-map-canvas-D7df2W27.js": "e74b72073a0149f70e3700809214b929bd88841577b63266199849b160ee9859",
|
||||
"apps/web/dist/assets/workplace-map-canvas-DLioOiRN.css": "b0cb7bd6fec5951c9c8f5808821438206604b1629a912c96009ac427f38e7d69",
|
||||
"apps/web/dist/index.html": "a34046a30a628b0f6a7d9d44d6951885ed130daced14c2de7dd683dffee3daf0"
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
{
|
||||
"recordedAtUtc": "2026-10-26T11:01:50.867076+00:00",
|
||||
"candidateCommit": "6992a4ac745acf6b474a0f22d1a859b861119452",
|
||||
"candidateBranch": "main",
|
||||
"initialWorkingTree": "clean",
|
||||
"verdict": "NO PREPARADO",
|
||||
"blockers": [
|
||||
"B-01: compiled server duplicate createRequire",
|
||||
"B-02: no Secure cookie in tested TLS-to-HTTP scenario"
|
||||
],
|
||||
"newFrontendVisualValidation": "pending",
|
||||
"newResourcesTests": {
|
||||
"pass": 124,
|
||||
"fail": 0
|
||||
},
|
||||
"newTableTests": {
|
||||
"pass": 9,
|
||||
"fail": 0
|
||||
},
|
||||
"goSourcesIdentical": false,
|
||||
"candidateChangesComparedUsingGitObjects": true,
|
||||
"concurrentChangesNotCertified": true,
|
||||
"productionConnected": false,
|
||||
"databaseConnected": false,
|
||||
"migrationsExecuted": false,
|
||||
"productionActivated": false,
|
||||
"commands": [
|
||||
{
|
||||
"command": "pnpm exec turbo run build --force",
|
||||
"exitCode": 0,
|
||||
"log": "technical/build.log"
|
||||
},
|
||||
{
|
||||
"command": "node --check apps/server/dist/index.js",
|
||||
"exitCode": 1,
|
||||
"log": "technical/node-check.log"
|
||||
},
|
||||
{
|
||||
"command": "Diagnóstico proxy-session-diagnostic.mjs en loopback, sin BD",
|
||||
"exitCode": 0,
|
||||
"log": "technical/proxy.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm --filter @metis/resources test",
|
||||
"exitCode": 0,
|
||||
"log": "technical/resources.log"
|
||||
},
|
||||
{
|
||||
"command": "Pruebas entity-column-visibility.test.tsx con aislamiento none",
|
||||
"exitCode": 0,
|
||||
"log": "technical/table.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm --filter @metis/resources typecheck",
|
||||
"exitCode": 0,
|
||||
"log": "technical/resources-types.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm --filter @metis/web typecheck",
|
||||
"exitCode": 0,
|
||||
"log": "technical/web-types.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm --filter @repo/rdx-ui typecheck",
|
||||
"exitCode": 0,
|
||||
"log": "technical/rdx-ui-types.log"
|
||||
},
|
||||
{
|
||||
"command": "Biome dirigido, 15 fuentes",
|
||||
"exitCode": 0,
|
||||
"log": "technical/biome.log"
|
||||
},
|
||||
{
|
||||
"command": "ESLint dirigido, 15 fuentes",
|
||||
"exitCode": 0,
|
||||
"log": "technical/eslint.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm typecheck",
|
||||
"exitCode": 2,
|
||||
"log": "technical/types.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm lint",
|
||||
"exitCode": 1,
|
||||
"log": "technical/lint.log"
|
||||
},
|
||||
{
|
||||
"command": "pnpm check",
|
||||
"exitCode": 1,
|
||||
"log": "technical/check.log"
|
||||
}
|
||||
],
|
||||
"files": {
|
||||
"builds.json": "f3c2db9da0f6d88c312c9111d37f8380b66ae19eb5fdd92e18f851e9f32aad66",
|
||||
"technical/biome.log": "39f4d8fe1255abee2e51cadc52a3613b308d03c4e646cd271c7ed6df407fbab6",
|
||||
"technical/build.log": "3ccaef5a89dc97a321913bb345a2c70b2e8ac36a679e9bff08c50caa952adfce",
|
||||
"technical/check.log": "15ad425f82d431d4a39aa0adf303c90a45a3f983b4c723eb10d26952cc5217d3",
|
||||
"technical/eslint.log": "2bd73ff0eb7555f4a8aac3ac9f64061e3b9f5bc50c0560cf8afd987ecf0fd00b",
|
||||
"technical/lint.log": "90b5583555e49b2cd54f6b284dd47df4086dcb0e85d50a688b65c7a0afb5c4b2",
|
||||
"technical/node-check.log": "8257d56c85b4657dfaaedc60781e41646a526c3bfc5b7e9ecc351f321f6eae8e",
|
||||
"technical/proxy.log": "b70d5360741b61f26b7f9a1366481b22acb55704f8a0d567b438686b614ccc69",
|
||||
"technical/rdx-ui-types.log": "29c5ee0f6b0b20dbb259b99d01276198c3d28190090706394608d4deda6d2c0b",
|
||||
"technical/resources-types.log": "29c5ee0f6b0b20dbb259b99d01276198c3d28190090706394608d4deda6d2c0b",
|
||||
"technical/resources.log": "1aa100050d30b4a19d43044fb5bf0b2b4e14fec26914cdd824bdd58e10bd663f",
|
||||
"technical/table.log": "25cd0182b29d78b97e8f35842e9a6ea1724f70bd2da65d058719f291b5d097ca",
|
||||
"technical/types.log": "6d534229b49c3f6329c466d76fd6cbce8bbf3afe5f22d35a67656ba3648d4fba",
|
||||
"technical/web-types.log": "29c5ee0f6b0b20dbb259b99d01276198c3d28190090706394608d4deda6d2c0b",
|
||||
"traceability.json": "4a006707c2705419377a2ea59f71a7c31e27a478b97785fc31793df56e28ab20"
|
||||
},
|
||||
"report": {
|
||||
"path": "docs/architecture/preflight-activacion-catalogo-global-sistemas-operativos.md",
|
||||
"sha256": "a9318a2168479b94c7f33f56c84543eb0baa7539b2d44d2eaea4a55f96c59cf6"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
Checked 15 files in 15ms. No fixes applied.
|
||||
@ -0,0 +1,47 @@
|
||||
• turbo 2.9.14
|
||||
|
||||
• Packages in scope: @metis/assets, @metis/companies, @metis/core, @metis/identity, @metis/relationship-contracts, @metis/resources, @metis/server, @metis/web, @repo/rdx-ddd, @repo/rdx-ui, @repo/rdx-utils, @repo/shadcn-ui, @repo/typescript-config
|
||||
• Running build in 13 packages
|
||||
• Remote caching disabled
|
||||
|
||||
@metis/server:build: cache bypass, force executing cd9cb21b866f866f
|
||||
@metis/web:build: cache bypass, force executing ca8ca295b02da9d1
|
||||
@metis/web:build: $ vite build
|
||||
@metis/server:build: $ tsup --config tsup.config.ts
|
||||
@metis/web:build: vite v8.3.0 building client environment for production...
|
||||
@metis/web:build: transforming...
|
||||
@metis/server:build: CLI Building entry: src/index.ts
|
||||
@metis/server:build: CLI Using tsconfig: tsconfig.json
|
||||
@metis/server:build: CLI tsup v8.5.1
|
||||
@metis/server:build: CLI Using tsup config: /home/david/dev/metis-desk/apps/server/tsup.config.ts
|
||||
@metis/server:build: CLI Target: node24
|
||||
@metis/server:build: CLI Cleaning output folder
|
||||
@metis/server:build: ESM Build start
|
||||
@metis/server:build: ESM dist/index.js 1.70 MB
|
||||
@metis/server:build: ESM dist/index.js.map 3.22 MB
|
||||
@metis/server:build: ESM ⚡️ Build success in 431ms
|
||||
@metis/web:build: ✓ 10068 modules transformed.
|
||||
@metis/web:build: rendering chunks...
|
||||
@metis/web:build: computing gzip size...
|
||||
@metis/web:build: dist/index.html 0.54 kB │ gzip: 0.35 kB
|
||||
@metis/web:build: dist/assets/geist-mono-latin-ext-variable-Bwz-egvJ.woff2 14.69 kB
|
||||
@metis/web:build: dist/assets/geist-mono-latin-variable-XN7g48iV.woff2 23.12 kB
|
||||
@metis/web:build: dist/assets/roboto-serif-latin-ext-variable-B9BEn9Tg.woff2 49.84 kB
|
||||
@metis/web:build: dist/assets/roboto-serif-latin-variable-CusprKnR.woff2 66.10 kB
|
||||
@metis/web:build: dist/assets/inter-variable-DiVDrmQJ.woff2 352.24 kB
|
||||
@metis/web:build: dist/assets/workplace-map-canvas-DLioOiRN.css 15.41 kB │ gzip: 2.56 kB
|
||||
@metis/web:build: dist/assets/index-ZFzBcHiV.css 253.08 kB │ gzip: 38.86 kB
|
||||
@metis/web:build: dist/assets/workplace-map-canvas-D7df2W27.js 227.75 kB │ gzip: 74.26 kB
|
||||
@metis/web:build: dist/assets/index-DsAXQkUa.js 1,820.59 kB │ gzip: 510.82 kB
|
||||
@metis/web:build:
|
||||
@metis/web:build: [plugin builtin:vite-reporter]
|
||||
@metis/web:build: (!) Some chunks are larger than 500 kB after minification. Consider:
|
||||
@metis/web:build: - Using dynamic import() to code-split the application
|
||||
@metis/web:build: - Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
|
||||
@metis/web:build: - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
|
||||
@metis/web:build: ✓ built in 1.05s
|
||||
|
||||
Tasks: 2 successful, 2 total
|
||||
Cached: 0 cached, 2 total
|
||||
Time: 1.788s
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,9 @@
|
||||
|
||||
/home/david/dev/metis-desk/modules/resources/src/common/locales/en.json
|
||||
0:0 warning File ignored because no matching configuration was supplied
|
||||
|
||||
/home/david/dev/metis-desk/modules/resources/src/common/locales/es.json
|
||||
0:0 warning File ignored because no matching configuration was supplied
|
||||
|
||||
✖ 2 problems (0 errors, 2 warnings)
|
||||
|
||||
@ -0,0 +1,471 @@
|
||||
$ biome lint .
|
||||
docs/auditorias/ficha-puesto-trabajo/implementacion-v3/mediciones.json lint ━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
i The size of the file is 2.5 MiB, which exceeds the configured maximum of 1.0 MiB for this project.
|
||||
Use the `files.maxSize` configuration to change the maximum size of files processed, or `files.includes` to ignore the file.
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:33:9 lint/complexity/useSimplifiedLogicExpression FIXABLE ━━━━━━━━━━
|
||||
|
||||
i Logical expression contains unnecessary complexity.
|
||||
|
||||
32 │ async save(companyId: string, personId: string, data: Uint8Array) {
|
||||
> 33 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
34 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
|
||||
i Safe fix: Reduce the complexity of the logical expression.
|
||||
|
||||
31 31 │
|
||||
32 32 │ async save(companyId: string, personId: string, data: Uint8Array) {
|
||||
33 │ - ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)
|
||||
33 │ + ····if·(!(z.uuid().safeParse(companyId).success·&&·z.uuid().safeParse(personId).success))
|
||||
34 34 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
35 35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:49:9 lint/complexity/useSimplifiedLogicExpression FIXABLE ━━━━━━━━━━
|
||||
|
||||
i Logical expression contains unnecessary complexity.
|
||||
|
||||
48 │ async remove(companyId: string, personId: string) {
|
||||
> 49 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
50 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
51 │
|
||||
|
||||
i Safe fix: Reduce the complexity of the logical expression.
|
||||
|
||||
47 47 │
|
||||
48 48 │ async remove(companyId: string, personId: string) {
|
||||
49 │ - ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)
|
||||
49 │ + ····if·(!(z.uuid().safeParse(companyId).success·&&·z.uuid().safeParse(personId).success))
|
||||
50 50 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
51 51 │
|
||||
|
||||
|
||||
modules/companies/src/api/infrastructure/person-photo-processor.ts:18:9 lint/complexity/useSimplifiedLogicExpression FIXABLE ━━━━━━━━━━
|
||||
|
||||
i Logical expression contains unnecessary complexity.
|
||||
|
||||
17 │ if (
|
||||
> 18 │ !metadata.width ||
|
||||
│ ^^^^^^^^^^^^^^^^^^
|
||||
> 19 │ !metadata.height ||
|
||||
│ ^^^^^^^^^^^^^^^^
|
||||
20 │ metadata.width > PERSON_PHOTO_LIMITS.inputDimension ||
|
||||
21 │ metadata.height > PERSON_PHOTO_LIMITS.inputDimension ||
|
||||
|
||||
i Safe fix: Reduce the complexity of the logical expression.
|
||||
|
||||
16 16 │
|
||||
17 17 │ if (
|
||||
18 │ - ········!metadata.width·||
|
||||
19 │ - ········!metadata.height·||
|
||||
18 │ + ········!(metadata.width·&&metadata.height·)·||
|
||||
20 19 │ metadata.width > PERSON_PHOTO_LIMITS.inputDimension ||
|
||||
21 20 │ metadata.height > PERSON_PHOTO_LIMITS.inputDimension ||
|
||||
|
||||
|
||||
.agents/skills/shadcn/assets/showcase/color-pair.tsx:81:17 lint/style/noNegationElse FIXABLE ━━━━━━━━━━
|
||||
|
||||
! Invert blocks when performing a negation test.
|
||||
|
||||
79 │ }, [])
|
||||
80 │
|
||||
> 81 │ const level = !info
|
||||
│ ^^^^^
|
||||
> 82 │ ? ""
|
||||
...
|
||||
> 88 │ ? "AA Large"
|
||||
> 89 │ : "Fail"
|
||||
│ ^^^^^^^^
|
||||
90 │
|
||||
91 │ return (
|
||||
|
||||
i Safe fix: Invert the condition and the blocks.
|
||||
|
||||
79 79 │ }, [])
|
||||
80 80 │
|
||||
81 │ - ··const·level·=·!info
|
||||
82 │ - ····?·""
|
||||
83 │ - ····:·info.ratio·>=·7
|
||||
81 │ + ··const·level·=·info
|
||||
82 │ + ····?·info.ratio·>=·7
|
||||
84 83 │ ? "AAA"
|
||||
85 84 │ : info.ratio >= 4.5
|
||||
······· │
|
||||
87 86 │ : info.ratio >= 3
|
||||
88 87 │ ? "AA Large"
|
||||
89 │ - ··········:·"Fail"
|
||||
88 │ + ··········:·"Fail"
|
||||
89 │ + ····:·""
|
||||
90 90 │
|
||||
91 91 │ return (
|
||||
|
||||
|
||||
.agents/skills/shadcn/assets/showcase/state-matrix.tsx:1:8 lint/style/useImportType FIXABLE ━━━━━━━━━━
|
||||
|
||||
× All these imports are only used as types.
|
||||
|
||||
> 1 │ import * as React from "react"
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
2 │
|
||||
3 │ import { cn } from "@/lib/utils"
|
||||
|
||||
i Importing the types with import type ensures that they are removed by the compilers and avoids loading unnecessary modules.
|
||||
|
||||
i Safe fix: Use import type.
|
||||
|
||||
1 │ import·type·*·as·React·from·"react"
|
||||
│ +++++
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:25:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
24 │ async get(personId: string) {
|
||||
> 25 │ if (!z.uuid().safeParse(personId).success)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 26 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
27 │ const photo = await this.repository.get(personId);
|
||||
28 │
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
23 23 │
|
||||
24 24 │ async get(personId: string) {
|
||||
25 │ - ····if·(!z.uuid().safeParse(personId).success)
|
||||
26 │ - ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
25 │ + ····if·(!z.uuid().safeParse(personId).success)·{
|
||||
26 │ + ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
27 │ + ····}
|
||||
27 28 │ const photo = await this.repository.get(personId);
|
||||
28 29 │
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:33:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
32 │ async save(companyId: string, personId: string, data: Uint8Array) {
|
||||
> 33 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 34 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
36 │ return Result.fail(new PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
31 31 │
|
||||
32 32 │ async save(companyId: string, personId: string, data: Uint8Array) {
|
||||
33 │ - ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)
|
||||
34 │ - ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
33 │ + ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)·{
|
||||
34 │ + ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
35 │ + ····}
|
||||
35 36 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
36 37 │ return Result.fail(new PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:35:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
33 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
34 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
> 35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 36 │ return Result.fail(new PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
37 │ if (!(await this.repository.exists(companyId, personId)))
|
||||
38 │ return Result.fail(new PersonPhotoError("PERSON_NOT_FOUND"));
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
33 33 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
34 34 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
35 │ - ····if·(data.length·>·PERSON_PHOTO_LIMITS.inputBytes)
|
||||
36 │ - ······return·Result.fail(new·PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
35 │ + ····if·(data.length·>·PERSON_PHOTO_LIMITS.inputBytes)·{
|
||||
36 │ + ······return·Result.fail(new·PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
37 │ + ····}
|
||||
37 38 │ if (!(await this.repository.exists(companyId, personId)))
|
||||
38 39 │ return Result.fail(new PersonPhotoError("PERSON_NOT_FOUND"));
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:37:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
36 │ return Result.fail(new PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
> 37 │ if (!(await this.repository.exists(companyId, personId)))
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 38 │ return Result.fail(new PersonPhotoError("PERSON_NOT_FOUND"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
39 │ const processed = await this.processor.optimize(data);
|
||||
40 │
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
35 35 │ if (data.length > PERSON_PHOTO_LIMITS.inputBytes)
|
||||
36 36 │ return Result.fail(new PersonPhotoError("PHOTO_TOO_LARGE"));
|
||||
37 │ - ····if·(!(await·this.repository.exists(companyId,·personId)))
|
||||
38 │ - ······return·Result.fail(new·PersonPhotoError("PERSON_NOT_FOUND"));
|
||||
37 │ + ····if·(!(await·this.repository.exists(companyId,·personId)))·{
|
||||
38 │ + ······return·Result.fail(new·PersonPhotoError("PERSON_NOT_FOUND"));
|
||||
39 │ + ····}
|
||||
39 40 │ const processed = await this.processor.optimize(data);
|
||||
40 41 │
|
||||
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:41:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
39 │ const processed = await this.processor.optimize(data);
|
||||
40 │
|
||||
> 41 │ if (!processed.isSuccess) return Result.fail(processed.error);
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
42 │
|
||||
43 │ return (await this.repository.replace(companyId, personId, processed.data))
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
41 │ ····if·(!processed.isSuccess)·{·return·Result.fail(processed.error);·}
|
||||
│ ++ ++
|
||||
|
||||
modules/companies/src/api/application/person-photo.use-cases.ts:49:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
48 │ async remove(companyId: string, personId: string) {
|
||||
> 49 │ if (!z.uuid().safeParse(companyId).success || !z.uuid().safeParse(personId).success)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 50 │ return Result.fail(new PersonPhotoError("INVALID_REQUEST"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
51 │
|
||||
52 │ return (await this.repository.replace(companyId, personId, null))
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
47 47 │
|
||||
48 48 │ async remove(companyId: string, personId: string) {
|
||||
49 │ - ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)
|
||||
50 │ - ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
49 │ + ····if·(!z.uuid().safeParse(companyId).success·||·!z.uuid().safeParse(personId).success)·{
|
||||
50 │ + ······return·Result.fail(new·PersonPhotoError("INVALID_REQUEST"));
|
||||
51 │ + ····}
|
||||
51 52 │
|
||||
52 53 │ return (await this.repository.replace(companyId, personId, null))
|
||||
|
||||
|
||||
modules/companies/src/api/application/process.use-cases.ts:127:5 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
125 │ const process = UniqueID.create(id);
|
||||
126 │
|
||||
> 127 │ if (!(company.isSuccess && process.isSuccess))
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 128 │ return Promise.resolve(Result.fail(new ProcessError("INVALID_REQUEST")));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
129 │
|
||||
130 │ return this.repository.remove(company.data, process.data);
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
125 125 │ const process = UniqueID.create(id);
|
||||
126 126 │
|
||||
127 │ - ····if·(!(company.isSuccess·&&·process.isSuccess))
|
||||
128 │ - ······return·Promise.resolve(Result.fail(new·ProcessError("INVALID_REQUEST")));
|
||||
127 │ + ····if·(!(company.isSuccess·&&·process.isSuccess))·{
|
||||
128 │ + ······return·Promise.resolve(Result.fail(new·ProcessError("INVALID_REQUEST")));
|
||||
129 │ + ····}
|
||||
129 130 │
|
||||
130 131 │ return this.repository.remove(company.data, process.data);
|
||||
|
||||
|
||||
modules/companies/src/api/infrastructure/persistence/migrate-people.ts:11:3 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
9 │ const columns = await query.describeTable("company_contacts");
|
||||
10 │
|
||||
> 11 │ if (!columns.photoVersion)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 12 │ await query.addColumn("company_contacts", "photoVersion", {
|
||||
> 13 │ type: DataTypes.STRING(64),
|
||||
> 14 │ allowNull: true,
|
||||
> 15 │ });
|
||||
│ ^^^
|
||||
16 │ for (const [name, length] of [
|
||||
17 │ ["position", 100],
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
9 9 │ const columns = await query.describeTable("company_contacts");
|
||||
10 10 │
|
||||
11 │ - ··if·(!columns.photoVersion)
|
||||
11 │ + ··if·(!columns.photoVersion)·{
|
||||
12 12 │ await query.addColumn("company_contacts", "photoVersion", {
|
||||
13 13 │ type: DataTypes.STRING(64),
|
||||
14 14 │ allowNull: true,
|
||||
15 │ - ····});
|
||||
15 │ + ····});
|
||||
16 │ + ··}
|
||||
16 17 │ for (const [name, length] of [
|
||||
17 18 │ ["position", 100],
|
||||
|
||||
|
||||
modules/companies/src/api/infrastructure/persistence/person-photo-migration.ts:10:3 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
8 │ );
|
||||
9 │
|
||||
> 10 │ if (rows.some((row) => row.name === "person_photos")) return;
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
11 │ await database
|
||||
12 │ .getQueryInterface()
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
10 │ ··if·(rows.some((row)·=>·row.name·===·"person_photos"))·{·return;·}
|
||||
│ ++ ++
|
||||
|
||||
modules/companies/src/api/infrastructure/persistence/sequelize-person-photo.repository.ts:49:7 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
47 │ });
|
||||
48 │
|
||||
> 49 │ if (!person) return false;
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
50 │ if (photo)
|
||||
51 │ await PersonPhotoModel.upsert(
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
49 │ ······if·(!person)·{·return·false;·}
|
||||
│ ++ ++
|
||||
|
||||
modules/companies/src/api/infrastructure/persistence/sequelize-person-photo.repository.ts:50:7 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
49 │ if (!person) return false;
|
||||
> 50 │ if (photo)
|
||||
│ ^^^^^^^^^^
|
||||
> 51 │ await PersonPhotoModel.upsert(
|
||||
> 52 │ { personId, companyId, data: Buffer.from(photo.data), version: photo.version },
|
||||
> 53 │ { transaction }
|
||||
> 54 │ );
|
||||
> 55 │ else await PersonPhotoModel.destroy({ where: { personId }, transaction });
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
56 │ await person.update({ photoVersion: photo?.version ?? null }, { transaction });
|
||||
57 │
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
48 48 │
|
||||
49 49 │ if (!person) return false;
|
||||
50 │ - ······if·(photo)
|
||||
50 │ + ······if·(photo)·{
|
||||
51 51 │ await PersonPhotoModel.upsert(
|
||||
52 52 │ { personId, companyId, data: Buffer.from(photo.data), version: photo.version },
|
||||
53 53 │ { transaction }
|
||||
54 │ - ········);
|
||||
54 │ + ········);
|
||||
55 │ + ······}
|
||||
55 56 │ else await PersonPhotoModel.destroy({ where: { personId }, transaction });
|
||||
56 57 │ await person.update({ photoVersion: photo?.version ?? null }, { transaction });
|
||||
|
||||
|
||||
modules/companies/src/api/infrastructure/persistence/sequelize-person-photo.repository.ts:55:7 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
53 │ { transaction }
|
||||
54 │ );
|
||||
> 55 │ else await PersonPhotoModel.destroy({ where: { personId }, transaction });
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
56 │ await person.update({ photoVersion: photo?.version ?? null }, { transaction });
|
||||
57 │
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
55 │ ······else·{·await·PersonPhotoModel.destroy({·where:·{·personId·},·transaction·});·}
|
||||
│ ++ ++
|
||||
|
||||
modules/companies/src/api/infrastructure/person-photo-processor.ts:17:7 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
15 │ }).metadata();
|
||||
16 │
|
||||
> 17 │ if (
|
||||
│ ^^^^
|
||||
> 18 │ !metadata.width ||
|
||||
...
|
||||
> 23 │ )
|
||||
> 24 │ return Result.fail(new PersonPhotoError("INVALID_PHOTO"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
25 │ const output = await sharp(input)
|
||||
26 │ .rotate()
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
21 21 │ metadata.height > PERSON_PHOTO_LIMITS.inputDimension ||
|
||||
22 22 │ !["jpeg", "png", "webp"].includes(metadata.format ?? "")
|
||||
23 │ - ······)
|
||||
24 │ - ········return·Result.fail(new·PersonPhotoError("INVALID_PHOTO"));
|
||||
23 │ + ······)·{
|
||||
24 │ + ········return·Result.fail(new·PersonPhotoError("INVALID_PHOTO"));
|
||||
25 │ + ······}
|
||||
25 26 │ const output = await sharp(input)
|
||||
26 27 │ .rotate()
|
||||
|
||||
|
||||
modules/companies/src/api/infrastructure/person-photo-processor.ts:34:7 lint/style/useBlockStatements FIXABLE ━━━━━━━━━━
|
||||
|
||||
× Block statements are preferred in this position.
|
||||
|
||||
32 │ .toBuffer();
|
||||
33 │
|
||||
> 34 │ if (output.length > PERSON_PHOTO_LIMITS.outputBytes)
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 35 │ return Result.fail(new PersonPhotoError("INVALID_PHOTO"));
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
36 │
|
||||
37 │ return Result.ok({
|
||||
|
||||
i Unsafe fix: Wrap the statement with a `JsBlockStatement`
|
||||
|
||||
32 32 │ .toBuffer();
|
||||
33 33 │
|
||||
34 │ - ······if·(output.length·>·PERSON_PHOTO_LIMITS.outputBytes)
|
||||
35 │ - ········return·Result.fail(new·PersonPhotoError("INVALID_PHOTO"));
|
||||
34 │ + ······if·(output.length·>·PERSON_PHOTO_LIMITS.outputBytes)·{
|
||||
35 │ + ········return·Result.fail(new·PersonPhotoError("INVALID_PHOTO"));
|
||||
36 │ + ······}
|
||||
36 37 │
|
||||
37 38 │ return Result.ok({
|
||||
|
||||
|
||||
The number of diagnostics exceeds the limit allowed. Use --max-diagnostics to increase it.
|
||||
Diagnostics not shown: 1.
|
||||
Checked 2231 files in 995ms. No fixes applied.
|
||||
Found 15 errors.
|
||||
Found 1 warning.
|
||||
Found 5 infos.
|
||||
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
× Some errors were emitted while running checks.
|
||||
|
||||
|
||||
[ELIFECYCLE] Command failed with exit code 1.
|
||||
@ -0,0 +1,8 @@
|
||||
/home/david/dev/metis-desk/apps/server/dist/index.js:23345
|
||||
import { createRequire } from "module";
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
SyntaxError: Identifier 'createRequire' has already been declared
|
||||
at checkSyntax (node:internal/main/check_syntax:72:5)
|
||||
|
||||
Node.js v24.20.0
|
||||
@ -0,0 +1 @@
|
||||
{"diagnostic":"TLS termination followed by HTTP to backend","trustProxy":false,"http":200,"secure":false,"sessionCookieIssued":false,"databaseUsed":false}
|
||||
@ -0,0 +1 @@
|
||||
$ tsc -p tsconfig.json
|
||||
@ -0,0 +1 @@
|
||||
$ tsc -p tsconfig.json
|
||||
@ -0,0 +1,133 @@
|
||||
$ TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/application/application-coincidence.test.ts src/api/domain/resource-catalog.test.ts src/api/domain/resource.test.ts src/api/domain/resource-template-propagation.test.ts src/api/application/resource-lifecycle.test.ts src/web/catalog/shared/catalog.test.tsx src/web/resources/shared/resources.test.tsx src/web/resources/list/helpers/resource-columns.test.tsx src/web/resources/list/helpers/resource-list-preferences.test.tsx src/web/resources/shared/resource-values.test.ts src/api/domain/template-sections.test.ts src/web/catalog-fields/select/catalog-field-selection.test.tsx src/api/domain/directory-domain.test.ts src/api/domain/network-share.test.ts src/api/domain/resource-scope.test.ts src/api/infrastructure/persistence/resource-scope-reader.test.ts src/api/application/resource-scope.test.ts src/api/application/resource-reference-contracts.test.ts src/api/domain/global-resource.test.ts src/web/global-applications/shared/global-applications.test.tsx src/api/domain/operating-system-identity.test.ts src/api/application/global-operating-system-contracts.test.ts src/api/application/global-operating-system-policy.test.ts src/web/resources/shared/ui/resource-characteristic-control.test.tsx
|
||||
✔ R2.6: nombre y proveedor son señales, nunca equivalencia confirmada (1.231616ms)
|
||||
✔ R2.6: preserva tildes, puntuación, abreviaturas, ediciones, versiones y números (0.376204ms)
|
||||
✔ R2.6: homónimos genéricos se mantienen débiles y los UUID nunca se sustituyen (22.305088ms)
|
||||
✔ F2: SO global público tipado, sin contaminación de aplicaciones ni perfiles (8.075287ms)
|
||||
✔ F2: elegibilidad de SO exige stock real y campos globales (0.322503ms)
|
||||
✔ F2: contrato 3 y gates de SO independientes; aplicaciones no abren SO (0.299603ms)
|
||||
✔ F3: cualquier escritor valida identidad publicada, corrección documental e historial (34.291168ms)
|
||||
✔ recurso: activar/desactivar conserva datos e historial y repetir estado no añade revisiones (12.676836ms)
|
||||
✔ recurso: valida identidades y rechaza actuar sobre recursos ajenos (2.464926ms)
|
||||
✔ recurso: propaga el bloqueo por asociaciones y permite desactivar (2.204824ms)
|
||||
✔ recurso: elimina y la repetición devuelve ausencia sin efectos adicionales (6.674072ms)
|
||||
✔ elegibilidad global exige serie exacta y campos globales; las copias no elevan ámbito (1.905821ms)
|
||||
✔ schemas discriminan owner, propósito, OS local y proyecciones privadas (12.243932ms)
|
||||
✔ requests acotan lotes, consulta, paginación y rechazan privilegios o integración (4.950553ms)
|
||||
✔ búsqueda literal conserva ñ, puntuación y espacios; pagina tras filtrar con desempate UUID (5.450259ms)
|
||||
✔ R2.5 admite gates configurables; los valores predeterminados siguen cerrados (0.405605ms)
|
||||
✔ R2.5: software y perfil se habilitan por separado y requieren catálogo (0.437904ms)
|
||||
✔ R2.5: perfil no depende de software ni abre asociaciones con catálogo cerrado (0.317003ms)
|
||||
✔ lectores locales: excluyen global y otra compañía incluso si el repositorio los entrega; stockKey no depende del nombre (17.228284ms)
|
||||
✔ HTTP actual sin MariaDB: aislamiento con 15 compañías, 50 recursos locales, alta, edición, estado e historial (396.325645ms)
|
||||
✔ DNS: normalización explícita, etiqueta única heredada, punto raíz y política ASCII IDN (2.207924ms)
|
||||
✔ capacidad estable: la plantilla renombrada sigue siendo dominio y una copia arbitraria no (2.272924ms)
|
||||
✔ alta, edición, DNS y archivo usan Resource y conservan su identidad e historial (6.923375ms)
|
||||
✔ catálogo: DNS obligatorio protegido, NetBIOS opcional e inicialización idempotente (15.121263ms)
|
||||
✔ identificadores de campo: mayúsculas no cambian identidad ni permiten duplicados (0.825809ms)
|
||||
✔ global: crear exige UUID v7 y conserva identidad y propietario en edición e idempotencia (12.355032ms)
|
||||
✔ global: captura por stockKey, sin depender del nombre traducido de la plantilla (3.562338ms)
|
||||
✔ SMB acepta \\NAS-01\Finanzas (2.271825ms)
|
||||
✔ SMB acepta \\files.example.local\Docs (0.293204ms)
|
||||
✔ SMB acepta \\192.168.5.10\C$ (0.164602ms)
|
||||
✔ SMB acepta \\samba\Diseño\Año 2026 (0.422204ms)
|
||||
✔ SMB acepta \\NAS\Docs\ (0.330904ms)
|
||||
✔ SMB rechaza "" (0.228603ms)
|
||||
✔ SMB rechaza "\\\\" (0.250303ms)
|
||||
✔ SMB rechaza "\\\\host" (0.133901ms)
|
||||
✔ SMB rechaza "\\\\\\share" (2.418226ms)
|
||||
✔ SMB rechaza "\\\\host\\\\" (0.550606ms)
|
||||
✔ SMB rechaza "C:\\Datos" (0.152901ms)
|
||||
✔ SMB rechaza "https://host/share" (0.100001ms)
|
||||
✔ SMB rechaza "\\\\host\\share\\.." (0.321703ms)
|
||||
✔ SMB rechaza "\\\\host\\share\\." (0.099201ms)
|
||||
✔ SMB rechaza "\\\\?\\UNC\\host\\share" (0.164002ms)
|
||||
✔ SMB rechaza "\\\\host\\share\\\\child" (0.210603ms)
|
||||
✔ SMB rechaza "\\\\host\\share." (0.262203ms)
|
||||
✔ SMB rechaza "\\\\host\\ share" (0.093101ms)
|
||||
✔ SMB rechaza "\\\\ho st\\share" (0.0716ms)
|
||||
✔ SMB rechaza "\\\\host\\bad\u0000" (0.092901ms)
|
||||
✔ SMB rechaza "\\\\host\\bad\ud800" (0.118101ms)
|
||||
✔ SMB rechaza "\\\\host\\xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" (0.0877ms)
|
||||
✔ SMB: comparación conservadora, alias y Unicode sin transformaciones destructivas (1.280214ms)
|
||||
✔ SMB: semántica por stockKey y protección del catálogo sin bloquear etiquetas u obligatoriedad (19.661911ms)
|
||||
✔ SMB: alta validada y diagnóstico histórico sin conversión ni revisión (17.875892ms)
|
||||
✔ SMB: nuevas altas fijan integración access y rechazan contexto incompatible (8.36469ms)
|
||||
✔ F1: identidad completa, edición conocida/no aplicable/desconocida e inactivos (4.554549ms)
|
||||
✔ F1: normalización conservadora no equipara ediciones, releases ni ausencia desconocida (0.822309ms)
|
||||
✔ F1: evolución de stock personalizada e idempotente; no elimina campos ni cambia obligatoriedad (32.804952ms)
|
||||
✔ F1: un catálogo anterior evoluciona sin sobrescribir bindings ni recursos particulares (14.087251ms)
|
||||
✔ catálogo de recursos: serie idempotente, UUID v7, características opcionales y protección (37.378401ms)
|
||||
✔ ámbitos, tipos, listas, campos externos y disponibilidad por compañía (8.182888ms)
|
||||
✔ licencias: ampliar un catálogo previo conserva identidades y disponibilidad (19.636311ms)
|
||||
✔ plantillas globales de serie editables: conserva identidad, asociaciones, disponibilidad y cambios al reiniciar (12.740137ms)
|
||||
✔ campos globales de serie editables: listas ampliables, tipo fijo y opciones existentes conservadas (11.259221ms)
|
||||
✔ sistema operativo: plantilla con uso explícito, copia y ampliación sin sobrescribir definiciones (8.40179ms)
|
||||
✔ iconos de recursos: todas las plantillas de serie tienen su icono y el arranque no repone los retirados (18.962003ms)
|
||||
✔ iconos de recursos: cambiar y retirar el icono conserva identidad y asociaciones, con guardado idempotente (11.14542ms)
|
||||
✔ iconos de recursos: claves desconocidas, cadenas vacías e iconos de campos se rechazan (7.788183ms)
|
||||
✔ ámbito: pares válidos y rechazo runtime de propietarios contradictorios o ausentes (14.326653ms)
|
||||
✔ ámbito: proyección preparada separada; HTTP local conserva exactamente su contrato (5.966463ms)
|
||||
✔ ámbito: edición y propagación locales conservan propietario, UUID e historia; globales son solo lectura (2.87503ms)
|
||||
✔ Resource: propaga altas, orden y retiradas conservando cero/falso, valores e historial; repetir no genera revisiones (29.87312ms)
|
||||
✔ Resource: los campos retirados no impiden añadir una plantilla completa y no se fusionan por nombre (5.910163ms)
|
||||
✔ Resource: retirar secciones conserva solo las necesarias y mover o reincorporar campos aplica el destino (0.721708ms)
|
||||
✔ recursos: tipos, valores opcionales y obligatorios, cero/falso y campos desconocidos (11.846427ms)
|
||||
✔ recurso: identidad estable, historial, desactivación e idempotencia (4.007143ms)
|
||||
✔ buzón como recurso: correo válido, cuenta opcional, cambios auditados y tipo fijo (3.488938ms)
|
||||
✔ secciones: nombres, identidades, referencias y un solo nivel (11.626625ms)
|
||||
✔ secciones: igualdad, orden, identidad y persistencia JSON (6.419668ms)
|
||||
✔ secciones: plantillas globales y locales conservan separación y UUID al editar (6.713372ms)
|
||||
✔ secciones de serie: el arranque conserva una plantilla sin grupos y su disponibilidad (21.021825ms)
|
||||
✔ especificación: secciones capturadas, edición posterior y obligatoriedad independiente (13.471245ms)
|
||||
✔ lector legacy: roundtrip conserva UUID, revisiones, valores, actor, fecha y DTO sin columna scope (18.6734ms)
|
||||
✔ lector: solo el adaptador legacy conocido interpreta ausencia; ningún ámbito inválido usa fallback (6.083365ms)
|
||||
✔ proyecciones operativas: global sintético se rechaza antes de SQL, el recurso local sin integración no crea proyección (99.064659ms)
|
||||
✔ escritor scoped: envía company explícito, preserva la salida legacy y rechaza global (1.365715ms)
|
||||
✔ selección múltiple añade un lote sin duplicados y conserva las reglas y el orden existentes (4.082543ms)
|
||||
✔ añadir desde una sección asigna solo los campos nuevos a su destino y conserva los existentes (1.319414ms)
|
||||
✔ la selección respeta el límite del schema, valida identificadores y rechaza el conjunto vacío (2.85603ms)
|
||||
✔ el listado reutiliza tabla, búsqueda y casillas traducidas y conserva las selecciones bloqueadas (108.042052ms)
|
||||
✔ L04.2: campos de recursos recuperan orden y página sin heredar otra pestaña (167.398384ms)
|
||||
✔ el catálogo muestra plantillas de serie y acción de alta sin formularios junto al listado (115.143825ms)
|
||||
✔ los formularios tienen ayuda inferior y selección de campos en diálogo con alta rápida (50.526938ms)
|
||||
✔ iconos de recursos: catálogo y selector traducido conservan la selección al editar y copiar (229.571942ms)
|
||||
✔ catálogo global: lectura mínima y navegación traducidas, sin administración para ordinarios (36.045985ms)
|
||||
✔ R2.4: mutación global invalida solo instalaciones cacheadas que usan su UUID (3.423236ms)
|
||||
✔ R2.5: cambio global invalida configuraciones afectadas y preserva otras compañías (1.158612ms)
|
||||
✔ SO global: lectura pública es/en conserva identidad estructurada y navegación propia (11.661124ms)
|
||||
✔ SO global: invalida software y su catálogo sin refrescar aplicaciones ni perfiles (1.589217ms)
|
||||
✔ columnas de características de recursos: identidad, campos nuevos de catálogo, instantáneas y nombres repetidos (23.45335ms)
|
||||
✔ columnas elegidas es/en: cero, falso, fecha localizada, unidades y guion para campos ajenos (73.833287ms)
|
||||
✔ los campos compartidos conservan un único ID y todos sus contextos de catálogo e instantánea (2.829631ms)
|
||||
✔ columnas generales e integración es/en: notas, fechas, autores y referencias semánticas (31.745039ms)
|
||||
✔ orden de recursos: aislamiento por compañía y pestaña, incluida Todo (16.513977ms)
|
||||
✔ orden de recursos: una URL explícita prevalece sobre la preferencia sin sobrescribirla (1.108812ms)
|
||||
✔ preferencias opcionales: datos inválidos y almacenamiento bloqueado no impiden listar (1.174612ms)
|
||||
✔ cambiar pestaña limpia el orden y la paginación anteriores conservando la búsqueda (0.422305ms)
|
||||
✔ la integración de buzón conserva su configuración explícita y no inventa referencias (3.047833ms)
|
||||
✔ los valores iniciales toleran catálogo pendiente y limitan el nombre sin cambiar la entrada (1.86692ms)
|
||||
✔ nombre propuesto: prioriza los identificadores de cada plantilla y limita su longitud (19.457508ms)
|
||||
✔ nombre propuesto: contempla campos personalizados y descarta valores no identificativos (1.662218ms)
|
||||
✔ paso de campos: exige valores obligatorios y acepta cero y respuestas negativas (0.523206ms)
|
||||
✔ paso de campos: requiere la dirección de buzón aunque la plantilla la marque opcional (13.524045ms)
|
||||
✔ recursos: listado con alta modal y sin formulario visible, en español e inglés (127.894465ms)
|
||||
✔ Recursos: las pestañas incluyen plantillas vacías e inactivas y combinan identidad y búsqueda (421.335414ms)
|
||||
✔ recurso: ayuda inferior, plantilla con alta rápida y sin asignaciones de recursos (17.78119ms)
|
||||
✔ licencia: formulario reutilizable con campos opcionales y ayuda antes de guardar (32.084143ms)
|
||||
✔ asistencia: alta de recurso con plantilla específica y características opcionales (6.850473ms)
|
||||
✔ alta por pasos: solo muestra la plantilla inicialmente y traduce su navegación (14.455054ms)
|
||||
✔ indicador de alta: conserva la plantilla elegida y explica el paso actual en ambos idiomas (12.246331ms)
|
||||
✔ recursos: resumen de plantilla con unidades, booleanos traducidos, cero y límite de cuatro (24.829866ms)
|
||||
✔ recursos: ficha lateral compacta traducida con cierre, acciones y contenido relacionado (18.940503ms)
|
||||
✔ recursos: inventario y panel usan el icono vigente de la plantilla sin cambiar la especificación (23.082547ms)
|
||||
✔ dominio: el formulario traduce el contexto pendiente y solo presenta un entorno confirmado (19.031704ms)
|
||||
✔ SMB: formulario explica acceso automático o histórico sin selectores incompatibles, es/en (9.3907ms)
|
||||
✔ campo declarativo: traduce opciones propias y conserva valores que coinciden con el prototipo (33.183154ms)
|
||||
ℹ tests 124
|
||||
ℹ suites 0
|
||||
ℹ pass 124
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 5080.204188
|
||||
@ -0,0 +1,17 @@
|
||||
✔ las columnas elegidas se restauran al volver a crear la tabla, incluidas las que llegan después (63.643378ms)
|
||||
✔ las preferencias quedan aisladas por lista y no se aplican cuando no se solicita persistencia (10.891116ms)
|
||||
✔ datos locales inválidos no rompen el listado y solo se aceptan valores booleanos (0.372704ms)
|
||||
✔ almacenamiento bloqueado o sin cuota permite seguir utilizando la selección local (0.403304ms)
|
||||
✔ buscar columnas ignora tildes y mayúsculas, conserva ñ y no modifica visibilidad (1.039011ms)
|
||||
✔ los grupos conservan orden, omiten vacíos y deduplican IDs sin perder columnas sin grupo (0.485705ms)
|
||||
✔ restaurar preferencias vacías recupera defaults actuales sin tocar otras tablas (9.4554ms)
|
||||
✔ los contadores distinguen visibilidad, total y coincidencias en es/en (0.262903ms)
|
||||
✔ listado remoto: recupera columnas por ámbito y protege nombre y acciones (20.791321ms)
|
||||
ℹ tests 9
|
||||
ℹ suites 0
|
||||
ℹ pass 9
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 1969.971224
|
||||
@ -0,0 +1,29 @@
|
||||
$ turbo run typecheck
|
||||
• turbo 2.9.14
|
||||
|
||||
• Packages in scope: @metis/assets, @metis/companies, @metis/core, @metis/identity, @metis/relationship-contracts, @metis/resources, @metis/server, @metis/web, @repo/rdx-ddd, @repo/rdx-ui, @repo/rdx-utils, @repo/shadcn-ui, @repo/typescript-config
|
||||
• Running typecheck in 13 packages
|
||||
• Remote caching disabled
|
||||
|
||||
@repo/rdx-utils:typecheck: cache hit, replaying logs e660b2f14d16d88c
|
||||
@repo/rdx-utils:typecheck: $ tsc -p tsconfig.json
|
||||
@repo/shadcn-ui:typecheck: cache miss, executing 3a02880bf260fd89
|
||||
@repo/rdx-ddd:typecheck: cache hit, replaying logs 315b0dccfb6d76d3
|
||||
@repo/rdx-ddd:typecheck: $ tsc -p tsconfig.json
|
||||
@metis/relationship-contracts:typecheck: cache hit, replaying logs 6ecbe82d1a36ae07
|
||||
@metis/relationship-contracts:typecheck: $ tsc -p tsconfig.json
|
||||
@repo/shadcn-ui:typecheck: $ tsc -p tsconfig.json
|
||||
@repo/shadcn-ui:typecheck: src/components/calendar.tsx(165,12): error TS2375: Type '{ day: CalendarDay; modifiers: Modifiers; disabled?: boolean | undefined; form?: string | undefined; formAction?: string | ((formData: FormData) => void | Promise<...>) | undefined; ... 285 more ...; locale: Partial<...> | undefined; }' is not assignable to type '{ locale?: Partial<DayPickerLocale>; }' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
|
||||
@repo/shadcn-ui:typecheck: Types of property 'locale' are incompatible.
|
||||
@repo/shadcn-ui:typecheck: Type 'Partial<DayPickerLocale> | undefined' is not assignable to type 'Partial<DayPickerLocale>'.
|
||||
@repo/shadcn-ui:typecheck: Type 'undefined' is not assignable to type 'Partial<DayPickerLocale>'.
|
||||
@repo/shadcn-ui:typecheck: [ELIFECYCLE] Command failed with exit code 2.
|
||||
ERROR @repo/shadcn-ui#typecheck: command (/home/david/dev/metis-desk/packages/shadcn-ui) /home/david/.nvm/versions/node/v24.20.0/bin/pnpm run typecheck exited (2)
|
||||
|
||||
Tasks: 3 successful, 4 total
|
||||
Cached: 3 cached, 4 total
|
||||
Time: 4.958s
|
||||
Failed: @repo/shadcn-ui#typecheck
|
||||
|
||||
ERROR run failed: command exited (2)
|
||||
[ELIFECYCLE] Command failed with exit code 2.
|
||||
@ -0,0 +1 @@
|
||||
$ tsc -p tsconfig.json
|
||||
@ -0,0 +1,112 @@
|
||||
{
|
||||
"head": "6992a4ac745acf6b474a0f22d1a859b861119452",
|
||||
"branch": "main",
|
||||
"initialWorkingTree": "clean",
|
||||
"baseGoHead": "d2bf856c973419277f0b30f6437242cd82f7d720",
|
||||
"unchangedRecordedSources": 1875,
|
||||
"changedRecordedSources": [
|
||||
{
|
||||
"path": "modules/resources/src/common/locales/en.json",
|
||||
"before": "a1dd1e416af87cd94ffa2c80855e9e809e3b62b1ddb679a4688d2adbd67ccc9e",
|
||||
"candidate": "38b1298bdee2c8f4f547669f5d766faaceb6d22412479da301fd96165f17f93b"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/common/locales/es.json",
|
||||
"before": "edc6151d0bfac31da3a1fed555231aa7eae5b7cb71779533ea503dbe38eb986c",
|
||||
"candidate": "f95606f21c1a25b58696a9d10e4853190af43894ff536971d59f31ece3ba008e"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/controllers/use-company-applications-controller.ts",
|
||||
"before": "050abdeb0f351ba85ea001ad94a450fc174d0d6d0c871da41949fee1732620a3",
|
||||
"candidate": "7ce972f6a5f0542656b096f886ae058bd92dc9aef7beee21e1e327a4348cc28f"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts",
|
||||
"before": "10d3101bbad439537af0fb6195e199484641d2a9bc59d50a1ddc29044ebe285c",
|
||||
"candidate": "72a55dedce077c6372272ef95b05575f9efced513b4e6ea912c4af423cf3cc11"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/ui/company-applications-list.tsx",
|
||||
"before": "73dced13ad6d1adaff9d10a1b8ad78a402e3b69fd54230d7c13c2a2353123660",
|
||||
"candidate": "e9290d45c77ed97f28c205e64358465a4e1cd6674bad393b2d0e1d11696b9162"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/ui/resource-inventory-table.tsx",
|
||||
"before": "de3d16f1df8dd869a1e692be3e35a23927475c5efdd5ec88aa1a651783052400",
|
||||
"candidate": "b3e42461e88e9e1941fedab69b6c2e49ca539fb9cb3d90ccd3cb529587da156b"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/ui/resources-page.tsx",
|
||||
"before": "d87fa2506625f88da5fdb42b79d046f06620e1e98cf278c28f55d87eae06d528",
|
||||
"candidate": "c9419e1661531f363a949473b0a41b5b345afdad3b8c0b9c70973e5c23379e2a"
|
||||
},
|
||||
{
|
||||
"path": "packages/rdx-ui/src/components/cursor-entity-table.tsx",
|
||||
"before": "6505c4f913b639038a71b41b169cd2382583359585795ac32fbbcfb4c92f25e7",
|
||||
"candidate": "9a5398dc82dd3935682c39e59e8e6a6e1e023f5fa60496afa6b2ee851fa335b2"
|
||||
},
|
||||
{
|
||||
"path": "packages/rdx-ui/src/entity-column-visibility.test.tsx",
|
||||
"before": "95aa7c35e5f70f8d61cd1ae5e08edfb591d24c3d85466d7eba4cdf2a4f3e3145",
|
||||
"candidate": "5ead7a4b1dd78a73a6ed54d1f7fa1238d33822496cc41391215edc1799aedc6d"
|
||||
}
|
||||
],
|
||||
"addedSources": [
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/controllers/use-resource-list-navigation.ts",
|
||||
"sha256": "7d773755a67618becca56290322668112fac7ec0a82373a23b5b904c589cc707"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/helpers/resource-additional-columns.tsx",
|
||||
"sha256": "21a6391388f44a486165dce21e98d0eec5a84414682d9a586751479a68c7078e"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/helpers/resource-characteristic-columns.ts",
|
||||
"sha256": "8865cbc2a88a235b17100d276be8a98d005cf948102b5816a73c1b711f75c6b0"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/helpers/resource-columns.test.tsx",
|
||||
"sha256": "38ef794093816780b13973f2a3d8444a73b0f2159bc89d5d1d50bb36aff54487"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/helpers/resource-list-preferences.test.tsx",
|
||||
"sha256": "5cddfa7c3dd91484d42e504e8d95898c3df2b8753dce2ce869a669a023b55f22"
|
||||
},
|
||||
{
|
||||
"path": "modules/resources/src/web/resources/list/helpers/resource-list-preferences.ts",
|
||||
"sha256": "e7bd6e27facdf032d18e1a5260b0e56cbe3950ccadd1e7bcee52836a5b5a7633"
|
||||
}
|
||||
],
|
||||
"workingGoDifferences": ["docs/componentes-de-interfaz.md", "modules/assets/src/web/index.ts"],
|
||||
"lockfileSha256": "bcd468ab1f127fffeb7f37ef3bdb5f2c5e85d40c192f3bc4a7cae5b2209b7c17",
|
||||
"functionalBackendChanges": false,
|
||||
"newVisualValidationRequired": true,
|
||||
"concurrentUnvalidatedSources": [
|
||||
{
|
||||
"path": "modules/assets/src/web/index.ts",
|
||||
"sha256": "d13dfe0b910d694643a878dff9d2439e7b68bc83045e6c3521f8a79e29453929"
|
||||
},
|
||||
{
|
||||
"path": "modules/assets/src/web/infrastructure/list/ui/infrastructure-page.tsx",
|
||||
"sha256": "e50db875ea633ceecc6763f4d4f4c683efadd3be2e85d5249d3f1fdc23e79198"
|
||||
},
|
||||
{
|
||||
"path": "modules/assets/src/web/infrastructure/shared/infrastructure.test.tsx",
|
||||
"sha256": "d73da6731f6de334f216fa901c65b5127b791916a93ab8c001153610025ddacf"
|
||||
},
|
||||
{
|
||||
"path": "modules/companies/src/web/companies/detail/controllers/use-company-detail-controller.ts",
|
||||
"sha256": "b774f445c5b97f31e42251df9a642b06661dc57ad26361eb8b9e8a5d0c54ef8c"
|
||||
},
|
||||
{
|
||||
"path": "modules/companies/src/web/companies/shared/companies-page.test.tsx",
|
||||
"sha256": "2db363fb74b145aed6a6749f62e1e0c77a6a1f77d410a1714aef82d79bb4a10f"
|
||||
},
|
||||
{
|
||||
"path": "modules/assets/src/web/infrastructure/shared/helpers/infrastructure-items.ts",
|
||||
"sha256": "e5455d36a326e82fb23ea68a74578dcd4938fa745b74cf81867522615e3b01e6"
|
||||
}
|
||||
],
|
||||
"recordedAtUtc": "2026-10-26T11:00:51.729096+00:00",
|
||||
"comparisonMethod": "GO hashes compared to git archive of candidate HEAD; subsequent workspace changes recorded separately"
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
Fecha: **26 de octubre de 2026**, Europe/Madrid. Inspección iniciada el día 25. Ámbito: repositorio local y diagnósticos locales sin BD. **No se ha conectado a producción, leído `.env`, ejecutado migraciones, modificado datos reales, cambiado gates ni desplegado.**
|
||||
|
||||
**Reevaluación vigente: commit `6992a4ac745acf6b474a0f22d1a859b861119452`, árbol inicialmente limpio, NO PREPARADO.** Los apartados 1–14 conservan el preflight del candidato anterior; el apartado 15 identifica las diferencias, las ejecuciones nuevas y el alcance actual de sus evidencias.
|
||||
|
||||
## 1. Resumen y dictamen
|
||||
|
||||
**NO PREPARADO para desplegar el artefacto de producción evaluado.** Se conserva la correspondencia de fuentes con el GO técnico, pero el preflight descubre dos bloqueos de ejecución/configuración que ese GO no había ensayado:
|
||||
@ -236,3 +238,76 @@ Responsables requeridos: mantenedor del build para B-01; backend y operación pa
|
||||
Condiciones antes de autorizar: artefacto compilado válido y arranque aislado comprobado; sesiones Secure a través de la topología aprobada; identidad de release congelada; destino identificado; inventario de clientes; metadata/datos verificados con autorización; backup/restauración aprobados; smoke y responsables acordados; tratamiento explícito de CI no verde. No cambiar flags como sustituto de cumplir estas condiciones.
|
||||
|
||||
**Dictamen final: NO PREPARADO**, referido al snapshot reproducido y al artefacto productivo generado localmente. El entorno real sigue pendiente. El informe prepara las decisiones y acciones necesarias; no solicita, concede ni ejecuta autorización de despliegue o activación.
|
||||
|
||||
## 15. Reevaluación del candidato confirmado en Git
|
||||
|
||||
Fecha: 26 de octubre de 2026. Solicitud reiterada de preflight; no autoriza corregir funcionalidad, migrar, desplegar ni activar. Se utilizó `rdx-solid` para limitar las comprobaciones a diferencias reales y a los bloqueos productivos ya detectados.
|
||||
|
||||
### 15.1. Identificación y diferencias respecto al GO
|
||||
|
||||
| Requisito | Estado actual | Evidencia |
|
||||
| --- | --- | --- |
|
||||
| Candidato actual | VERIFICADO | `main`, `6992a4ac745acf6b474a0f22d1a859b861119452`; `git status --short` vacío antes de intervenir |
|
||||
| Trazabilidad del código candidato | VERIFICADO | Ahora está confirmado en Git; reproducible mediante checkout del commit y dependencias fijadas. No necesita el overlay anterior para identificar sus fuentes |
|
||||
| Igualdad completa con el GO | NO CUMPLE | De 1884 fuentes registradas: **1875 iguales y 9 modificadas**; hay **6 fuentes adicionales** en los mismos ámbitos |
|
||||
| Archivos de trabajo del GO | VERIFICADO con diferencia documental | De los 40 registrados, 39 coinciden; cambió `docs/componentes-de-interfaz.md` |
|
||||
| Node/PNPM y dependencias | VERIFICADO en instalación/manifests | 24.20.0 / 11.25.0; lockfile conserva SHA-256 `bcd468ab1f127fffeb7f37ef3bdb5f2c5e85d40c192f3bc4a7cae5b2209b7c17` |
|
||||
| Instalación limpia de dependencias del destino | PENDIENTE DE VERIFICACIÓN | No se ejecuta install ni se cambia configuración privada |
|
||||
|
||||
Los commits `07082001` y `6992a4ac` incorporan el trabajo previamente sin commit y cambios adicionales. Se compararon contenidos con los manifiestos del GO; no se atribuye todo `git diff d2bf856c..HEAD` a una diferencia funcional posterior, porque también contiene aquel trabajo ya validado.
|
||||
|
||||
Durante la reevaluación aparecieron cambios concurrentes sin commit en `modules/assets/src/web/index.ts`, `infrastructure-page.tsx`, `use-company-detail-controller.ts`, el nuevo helper `infrastructure-items.ts` y pruebas `infrastructure.test.tsx` / `companies-page.test.tsx`. Se preservaron. **El candidato de esta tabla es el commit, no el árbol concurrente final**: sus diferencias se verificaron leyendo los objetos de `git archive HEAD`. Los hashes del trabajo concurrente se registran aparte como no validados en la fecha de captura; las ejecuciones de esta intervención no certifican esas modificaciones posteriores. Una release que las incorpore necesita nueva identificación y análisis de impacto.
|
||||
|
||||
| Diferencia | Clasificación | Impacto y validación |
|
||||
| --- | --- | --- |
|
||||
| Columnas adicionales de características/recursos y sus pruebas | Frontend | Cambia el contenido configurable del inventario; Resources reejecutado |
|
||||
| Preferencias por compañía/plantilla, precedencia de URL y persistencia de orden | Frontend | Afecta navegación y contexto de listados; pruebas nuevas incluidas en Resources |
|
||||
| `CursorEntityTable` y prueba de visibilidad | Frontend compartido | Puede afectar también listados globales; reejecutada la suite específica de columnas/tablas. Validación visual actual pendiente |
|
||||
| Traducciones es/en de Resources | Frontend/textos | Fuentes distintas del GO; no certificar con capturas anteriores |
|
||||
| Guía de listados y componentes | Documentación | Revisada como patrón vigente; no cambia esquema/contrato |
|
||||
| Script de test en `modules/resources/package.json` | Configuración de validación | Añade nuevas pruebas; no cambia dependencias runtime ni lockfile |
|
||||
| DTO HTTP, lógica backend, persistencia, seguridad, gates y configuración de build | Sin diferencia funcional identificada respecto al GO | Las diferencias de `common` son locales es/en; los nuevos archivos server son las fixtures/tests del cierre ya registrados en el GO |
|
||||
|
||||
Las nueve fuentes modificadas y seis nuevas, con hashes, están enumeradas en [traceability.json](../../.artifacts/resources-global-operating-systems-preflight-6992a4ac/traceability.json). No hay bloqueo de identificación del candidato. **No se traslada el GO íntegro al frontend cambiado**: corresponde validar visualmente los nuevos listados/componente compartido antes de una futura aprobación de release.
|
||||
|
||||
### 15.2. Artefactos y bloqueos reproducidos
|
||||
|
||||
Se ejecutó `pnpm exec turbo run build --force`: servidor y web compilan, dos tareas correctas. Se calcularon hashes de los 12 archivos dist. **El dist del servidor coincide byte a byte con el del preflight anterior; el dist web es distinto**. Por tanto, la correspondencia del frontend antiguo con MCP descrita en el apartado 3 no se aplica al candidato actual.
|
||||
|
||||
- **B-01 sigue NO CUMPLE:** nuevo `node --check apps/server/dist/index.js` devuelve 1, `Identifier 'createRequire' has already been declared`. `tsup.config.ts` conserva el banner y el bundle incluye el segundo import procedente de sharp. La compilación correcta no acredita ejecución; no se arrancó ese servidor ni se abrió BD.
|
||||
- **B-02 sigue NO CUMPLE para TLS→HTTP:** el diagnóstico conservado se ejecutó con el `createApp` actual. Resultado nuevo: `trustProxy=false`, `http=200`, `secure=false`, `sessionCookieIssued=false`, `databaseUsed=false`. Salida cero del diagnóstico significa que terminó el experimento, no que la sesión de producción funcione.
|
||||
|
||||
No se ha configurado confianza en proxy, relajado Secure, cambiado `NODE_ENV`, editado el bundle ni modificado sharp para eludir los bloqueos. La solución de TLS/proxy debe ser aprobada y acotada al proxy real, sin confiar indiscriminadamente en cabeceras reenviadas.
|
||||
|
||||
### 15.3. Validaciones dirigidas nuevas y deuda actual
|
||||
|
||||
| Comando / ámbito | Resultado nuevo |
|
||||
| --- | --- |
|
||||
| `pnpm --filter @metis/resources test`, con loopback permitido | **124/124**, incluido caché, contratos y pruebas nuevas de columnas/preferencias |
|
||||
| `node --import tsx --test --test-isolation=none src/entity-column-visibility.test.tsx`, desde rdx-ui con TSX_TSCONFIG_PATH del paquete | **9/9** |
|
||||
| Typecheck directo Resources, Web y rdx-ui | Los tres correctos |
|
||||
| Biome y ESLint sobre las 15 fuentes modificadas/nuevas | Ambos correctos |
|
||||
| Build forzado | Dos tareas correctas; advertencia de tamaño de chunk web |
|
||||
| Parse del servidor compilado | **Fallo bloqueante B-01**, salida 1 |
|
||||
| Diagnóstico de sesión local | Experimento completado; demuestra **B-02** |
|
||||
| `pnpm typecheck` raíz | Falla en calendario vendor por `locale` opcional, salida 2 |
|
||||
| `pnpm lint` raíz | 15 errores, 1 warning, 5 infos; salida 1 |
|
||||
| `pnpm check` raíz | 173 errores, 1 warning, 5 infos; salida 1 |
|
||||
|
||||
No se han corregido incidencias ajenas. Los propietarios y clasificación de deuda del apartado 12 siguen aplicándose: el parse inválido es bloqueante, no una excepción aceptable por antigüedad; los fallos generales requieren tratamiento explícito de CI. Los dos fallos previos de ayuda de Companies son antecedentes, no una ejecución nueva. No se repitió la suite completa; posteriormente se modificó concurrentemente `companies-page.test.tsx`, por lo que su estado en ese trabajo posterior queda pendiente, sin afirmar que sus dos fallos persistan o estén resueltos allí.
|
||||
|
||||
No se repitieron MariaDB/HTTP-SQL con fixtures ni Playwright con preparación persistente: no cambió el backend/contrato/persistencia y este preflight no autoriza escrituras de negocio o migraciones. Las 29/29 + 50/50 y las 35 comprobaciones/69 capturas siguen siendo antecedentes con su alcance anterior. Para el nuevo frontend se requiere validación visual dirigida de columnas, preferencias, contexto y listados globales, en entorno aislado autorizado para preparar datos; las pruebas de componente no sustituyen esa comprobación.
|
||||
|
||||
### 15.4. Entorno, gates, rollback y autorización
|
||||
|
||||
No hay nueva evidencia autorizada del destino. Motor/versión/esquema aplicado, plantilla real, FKs/CHECK/índices, clientes desplegados, proxy, sesiones, supervisor, backup/restore y valores efectivos de gates continúan **PENDIENTES DE VERIFICACIÓN**. No se infieren de defaults ni del compose de desarrollo. El SQL del apartado 6 sigue preparado y **no ejecutado**; sus tablas/migraciones no cambiaron.
|
||||
|
||||
Se verificó que los seis nombres de flags y sus dependencias siguen iguales. El plan de los apartados 8–11 sigue vigente como propuesta: persistencia compatible → lectores v3 → frontend coordinado → OS_CATALOG administrativo → OS_SOFTWARE, que abre **selección y asociación juntas**. No hay gate por compañía ni se añaden flags para simularlo. Mantener los cuatro flags application según su autorización independiente.
|
||||
|
||||
El rollback propuesto conserva lectores v3, UUID, productos, instalaciones e historiales; cierra nuevas dependencias y altas, permite las retiradas legítimas ensayadas y no activa D08.2. No es un procedimiento productivo ya ejecutado. Los smoke tests de lectura y escritura siguen separados; ninguna escritura, conexión al destino o activación se ejecutó en esta reevaluación.
|
||||
|
||||
### 15.5. Evidencias y dictamen vigente
|
||||
|
||||
Ejecuciones nuevas, diferencias y hashes: [manifiesto 6992a4ac](../../.artifacts/resources-global-operating-systems-preflight-6992a4ac/manifest.json). El preflight anterior y sus evidencias se conservan sin sobrescribirlos. Esta intervención modifica únicamente este informe y añade evidencias diagnósticas; los cambios concurrentes ajenos permanecen sin certificar.
|
||||
|
||||
**NO PREPARADO**, para `6992a4ac745acf6b474a0f22d1a859b861119452` y sus artefactos generados localmente. Primero deben resolverse B-01 y la configuración aplicable de B-02, verificarse el artefacto ejecutable y revalidarse el frontend cambiado. Después faltan las comprobaciones y autorizaciones del destino enumeradas arriba. El código está identificado, pero eso no acredita una release operativa ni autoriza publicarla.
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
# Componentes de interfaz
|
||||
|
||||
## Submenú de Personas en la navegación de compañía
|
||||
|
||||
`CompanySectionNavigation` compone `SidebarMenu` y `SidebarMenuItem` de shadcn/ui
|
||||
dentro del contenedor contextual existente. `CompanyPeopleNavigation` utiliza
|
||||
`SidebarMenuSub`, `SidebarMenuSubItem` y `SidebarMenuSubButton` con `BrowserLink`
|
||||
mediante `render`. Mantiene las tres vistas de Personas en el panel fijo y en el Sheet,
|
||||
con selección por URL, listas anidadas semánticas y expansión al entrar en la sección.
|
||||
No requiere otro SidebarProvider ni cambia el controlador responsive del shell.
|
||||
Los contadores de las entradas principales conservan `CountBadge` de rdx-ui.
|
||||
|
||||
Verificado el 26 de octubre de 2026 mediante Playwright MCP en una instalación
|
||||
desechable de `metis_desk_test`: 15 compañías y una principal con 50 personas,
|
||||
51 activos y 66 recursos. Las vistas presentan 12 contactos, 43 activas y 7 inactivas.
|
||||
Se comprobaron 1440×900, 390×844 y 320×640, teclado, selección, cierre del Sheet y
|
||||
retorno del foco, recarga, vuelta de ficha, búsqueda/orden, reinicio de página,
|
||||
vacío y limpieza de filtros. Evidencias locales en
|
||||
`.artifacts/playwright/authenticated-6t1tczdv/people-navigation*.{png,json}`;
|
||||
las capturas de escritorio y móvil se abrieron e inspeccionaron. No se declara
|
||||
una auditoría completa de accesibilidad ni de estados de error de red.
|
||||
|
||||
Skills consultadas: `ux-navigation` para jerarquía y orientación, `shadcn` para la
|
||||
composición mediante `render`, `vercel-react-best-practices` para derivar selección
|
||||
de la URL y `rdx-solid` para responsabilidades y pruebas. No se modificó vendor.
|
||||
|
||||
Metis Desk usa shadcn/ui, con Base UI y el estilo base-vega, siguiendo la organización de factuges-erp.
|
||||
`packages/shadcn-ui` contiene únicamente el código del registro oficial y no se adapta ni se
|
||||
extiende desde este proyecto. Se consume mediante `@repo/shadcn-ui`.
|
||||
|
||||
@ -23,6 +23,14 @@ autenticar se recupera ese destino. No se aceptan redirecciones a otros orígene
|
||||
|
||||
## Espacio de compañía
|
||||
|
||||
En Personas, `/companies/:companyId/people?contactsOnly=true` abre los contactos de la
|
||||
compañía; `?active=true` y `?active=false` muestran las personas activas e inactivas.
|
||||
Sin estos filtros se muestran todas. El menú despliega estas tres subentradas al entrar
|
||||
en Personas, tanto en el panel fijo como en el Sheet. Cada enlace conserva búsqueda y
|
||||
orden, reinicia la página y reemplaza los filtros de la vista anterior. El filtro de estado
|
||||
del listado comparte la misma URL; puede combinarse con Solo contactos. La ficha y el
|
||||
retorno conservan ambos criterios. `active` no habilita cursor ni pageSize en Personas.
|
||||
|
||||
Todas las secciones siguientes cuelgan de `/companies/:companyId`:
|
||||
|
||||
```text
|
||||
|
||||
@ -14,6 +14,7 @@ export {
|
||||
infrastructureApi,
|
||||
infrastructureKeys,
|
||||
} from "./infrastructure/shared/api/infrastructure-api";
|
||||
export { getInfrastructureItems } from "./infrastructure/shared/helpers/infrastructure-items";
|
||||
export { NetworkSharePublications } from "./network-share-publications/detail/ui/network-share-publications";
|
||||
export { PublicationPicker } from "./network-share-publications/select/ui/publication-picker";
|
||||
export { assetsEnglish, assetsSpanish } from "./shared";
|
||||
|
||||
@ -17,6 +17,7 @@ import { InventoryFeedback } from "../../../shared";
|
||||
import { inventoryApi } from "../../../shared/api";
|
||||
import { inventoryKeys } from "../../../shared/queries";
|
||||
import { infrastructureApi, infrastructureKeys } from "../../shared/api/infrastructure-api";
|
||||
import { getInfrastructureItems } from "../../shared/helpers/infrastructure-items";
|
||||
import { infrastructureRoles } from "../../shared/helpers/infrastructure-roles";
|
||||
import { InfrastructureEditorDialog } from "../../shared/ui/infrastructure-editor-dialog";
|
||||
|
||||
@ -39,16 +40,11 @@ export function InfrastructurePage({ companyId }: { companyId: string }) {
|
||||
});
|
||||
const assets = inventory.data?.assets ?? [];
|
||||
const editing = assets.find((asset) => asset.id === editingId);
|
||||
const items = assets.flatMap((asset) => {
|
||||
const config = network.data?.items.find((item) => item.assetId === asset.id);
|
||||
|
||||
const roles = infrastructureRoles(asset.values.templateId, catalog.data?.items ?? [], config);
|
||||
|
||||
return (roles.length || config?.values.interfaces.length) &&
|
||||
(!role || roles.some((value) => value === role))
|
||||
? [{ ...asset, config: { roles, interfaces: config?.values.interfaces ?? [] } }]
|
||||
: [];
|
||||
});
|
||||
const items = getInfrastructureItems(
|
||||
assets,
|
||||
catalog.data?.items ?? [],
|
||||
network.data?.items ?? []
|
||||
).filter((asset) => !role || asset.config.roles.some((value) => value === role));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import type { CatalogItemDTO } from "../../../../common";
|
||||
import type { InfrastructureDTO } from "../../../../common/infrastructure";
|
||||
import type { AssetDTO } from "../../../../common/inventory";
|
||||
|
||||
import { infrastructureRoles } from "./infrastructure-roles";
|
||||
|
||||
/** Activos que pertenecen a infraestructura, con su clasificación e interfaces efectivas. */
|
||||
export function getInfrastructureItems(
|
||||
assets: readonly AssetDTO[],
|
||||
catalog: readonly CatalogItemDTO[],
|
||||
configurations: readonly InfrastructureDTO[]
|
||||
) {
|
||||
return assets.flatMap((asset) => {
|
||||
const config = configurations.find((item) => item.assetId === asset.id);
|
||||
const roles = infrastructureRoles(asset.values.templateId, catalog, config);
|
||||
|
||||
return roles.length || config?.values.interfaces.length
|
||||
? [{ ...asset, config: { roles, interfaces: config?.values.interfaces ?? [] } }]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
@ -20,6 +20,7 @@ import { inventoryKeys } from "../../shared/queries";
|
||||
import { InfrastructurePage } from "../list/ui/infrastructure-page";
|
||||
|
||||
import { infrastructureKeys } from "./api/infrastructure-api";
|
||||
import { getInfrastructureItems } from "./helpers/infrastructure-items";
|
||||
import { infrastructureRoles } from "./helpers/infrastructure-roles";
|
||||
import { AssetInfrastructurePanel } from "./ui/asset-infrastructure-panel";
|
||||
|
||||
@ -117,6 +118,19 @@ test("infraestructura: clasificación de serie independiente del nombre/icono y
|
||||
assert.deepEqual(infrastructureRoles(templateId, [{ ...renamed, builtinKey: null }]), []);
|
||||
});
|
||||
|
||||
test("infraestructura: el total incluye roles e interfaces, respeta exclusiones manuales y no duplica activos", () => {
|
||||
assert.equal(getInfrastructureItems([], catalog.items, []).length, 0);
|
||||
assert.equal(getInfrastructureItems([asset], catalog.items, []).length, 1);
|
||||
assert.equal(getInfrastructureItems([asset], catalog.items, [config]).length, 1);
|
||||
assert.equal(getInfrastructureItems([asset], [], []).length, 0);
|
||||
const interfacesOnly = { ...config, values: { ...config.values, roles: [] } };
|
||||
const excluded = { ...config, values: { ...config.values, roles: [], interfaces: [] } };
|
||||
|
||||
assert.equal(getInfrastructureItems([asset], [], [interfacesOnly]).length, 1);
|
||||
assert.equal(getInfrastructureItems([asset], catalog.items, [excluded]).length, 0);
|
||||
assert.equal(getInfrastructureItems([asset], [], [{ ...config, assetId: companyId }]).length, 0);
|
||||
});
|
||||
|
||||
test("infraestructura: listado y ficha presentan interfaces/IP en español e inglés, sin puertos", async () => {
|
||||
for (const [language, title, edit] of [
|
||||
["es", "Infraestructura", "Editar infraestructura"],
|
||||
|
||||
@ -26,6 +26,12 @@
|
||||
"close": "Close navigation",
|
||||
"skip": "Skip to content"
|
||||
},
|
||||
"peopleNavigation": {
|
||||
"contacts": "Company contacts",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"all": "All people"
|
||||
},
|
||||
"navigationCounts": {
|
||||
"loading": "Loading total…",
|
||||
"error": "The total could not be refreshed."
|
||||
|
||||
@ -26,6 +26,12 @@
|
||||
"close": "Cerrar navegación",
|
||||
"skip": "Saltar al contenido"
|
||||
},
|
||||
"peopleNavigation": {
|
||||
"contacts": "Contactos de la compañía",
|
||||
"active": "Activas",
|
||||
"inactive": "Inactivas",
|
||||
"all": "Todas las personas"
|
||||
},
|
||||
"navigationCounts": {
|
||||
"loading": "Cargando total…",
|
||||
"error": "No se ha podido actualizar el total."
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import {
|
||||
catalogApi as assetCatalogApi,
|
||||
catalogKeys as assetCatalogKeys,
|
||||
inventoryApi as assetInventoryApi,
|
||||
inventoryKeys as assetInventoryKeys,
|
||||
getInfrastructureItems,
|
||||
infrastructureApi,
|
||||
infrastructureKeys,
|
||||
} from "@metis/assets/client";
|
||||
import { useListQuery } from "@metis/core/client";
|
||||
import {
|
||||
@ -41,6 +46,16 @@ export function useCompanyDetailController({
|
||||
queryFn: () => resourceInventoryApi.get(id),
|
||||
enabled: Boolean(company),
|
||||
});
|
||||
const infrastructure = useListQuery({
|
||||
queryKey: infrastructureKeys.company(id),
|
||||
queryFn: () => infrastructureApi.get(id),
|
||||
enabled: Boolean(company),
|
||||
});
|
||||
const assetCatalog = useListQuery({
|
||||
queryKey: assetCatalogKeys.scope(id),
|
||||
queryFn: () => assetCatalogApi.get(id),
|
||||
enabled: Boolean(company),
|
||||
});
|
||||
const profiles = useListQuery({
|
||||
queryKey: profileKeys.company(id),
|
||||
queryFn: () => profileApi.get(id),
|
||||
@ -51,6 +66,17 @@ export function useCompanyDetailController({
|
||||
departments: { count: company?.departments.length, failed: false },
|
||||
sites: { count: company?.sites.length, failed: false },
|
||||
inventory: { count: inventory.data?.assets.length, failed: inventory.isError },
|
||||
infrastructure: {
|
||||
count:
|
||||
inventory.data && infrastructure.data && assetCatalog.data
|
||||
? getInfrastructureItems(
|
||||
inventory.data.assets,
|
||||
assetCatalog.data.items,
|
||||
infrastructure.data.items
|
||||
).length
|
||||
: undefined,
|
||||
failed: inventory.isError || infrastructure.isError || assetCatalog.isError,
|
||||
},
|
||||
workstations: { count: inventory.data?.workstations.length, failed: inventory.isError },
|
||||
resources: { count: resources.data?.resources.length, failed: resources.isError },
|
||||
profiles: { count: profiles.data?.profiles.length, failed: profiles.isError },
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
import { BrowserLink } from "@metis/core/client";
|
||||
import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@repo/shadcn-ui/components/sidebar";
|
||||
import type { MouseEventHandler } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { companySectionHref } from "../../../workspace/shared/helpers";
|
||||
|
||||
/** Vistas del listado de Personas; cada destino conserva búsqueda y orden, y reinicia página. */
|
||||
export function CompanyPeopleNavigation({
|
||||
companyId,
|
||||
searchParams,
|
||||
onNavigate,
|
||||
}: {
|
||||
companyId: string;
|
||||
searchParams: URLSearchParams;
|
||||
onNavigate?: MouseEventHandler<HTMLElement> | undefined;
|
||||
}) {
|
||||
const { t } = useTranslation("companies");
|
||||
const contactsOnly = searchParams.get("contactsOnly") === "true";
|
||||
const active = searchParams.get("active");
|
||||
const entries = [
|
||||
{ key: "contacts", label: t("peopleNavigation.contacts"), selected: contactsOnly },
|
||||
{
|
||||
key: "active",
|
||||
label: t("peopleNavigation.active"),
|
||||
selected: !contactsOnly && active === "true",
|
||||
},
|
||||
{
|
||||
key: "inactive",
|
||||
label: t("peopleNavigation.inactive"),
|
||||
selected: !contactsOnly && active === "false",
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<SidebarMenuSub aria-label={t("people")} className="my-1 border-border">
|
||||
{entries.map((entry) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
params.delete("page");
|
||||
params.delete("contactsOnly");
|
||||
params.delete("active");
|
||||
if (entry.key === "contacts") {
|
||||
params.set("contactsOnly", "true");
|
||||
} else {
|
||||
params.set("active", String(entry.key === "active"));
|
||||
}
|
||||
const href = `${companySectionHref(companyId, "people")}?${params.toString()}`;
|
||||
|
||||
return (
|
||||
<SidebarMenuSubItem key={entry.key}>
|
||||
<SidebarMenuSubButton
|
||||
aria-current={entry.selected ? "page" : undefined}
|
||||
className="h-auto min-h-10 whitespace-normal py-2 text-foreground hover:bg-accent hover:text-accent-foreground data-active:bg-accent data-active:text-accent-foreground"
|
||||
isActive={entry.selected}
|
||||
render={<BrowserLink href={href} onClick={onNavigate} />}
|
||||
>
|
||||
{entry.label}
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import { BrowserLink } from "@metis/core/client";
|
||||
import { BrowserLink, useBrowserSearchParams } from "@metis/core/client";
|
||||
import { CountBadge } from "@repo/rdx-ui/components/count-badge";
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@repo/rdx-ui/lib/icons";
|
||||
import { Button, buttonVariants } from "@repo/shadcn-ui/components/button";
|
||||
import { SidebarMenu, SidebarMenuItem } from "@repo/shadcn-ui/components/sidebar";
|
||||
import type { MouseEventHandler } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@ -8,6 +10,8 @@ import { companySectionHref } from "../../../workspace/shared/helpers";
|
||||
import type { CompanyTab } from "../../shared/types";
|
||||
import type { useCompanyNavigationEntries } from "../helpers/use-company-navigation-entries";
|
||||
|
||||
import { CompanyPeopleNavigation } from "./company-people-navigation";
|
||||
|
||||
export function CompanySectionNavigation({
|
||||
companyId,
|
||||
activeSection,
|
||||
@ -26,6 +30,14 @@ export function CompanySectionNavigation({
|
||||
onSectionChange?: (section: CompanyTab) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation("companies");
|
||||
const { searchParams } = useBrowserSearchParams();
|
||||
const peopleExpanded = activeSection === "people";
|
||||
const PeopleChevron = peopleExpanded ? ChevronDownIcon : ChevronRightIcon;
|
||||
const itemClassName =
|
||||
layout === "sidebar"
|
||||
? "group/company-section cursor-pointer text-foreground hover:text-primary h-auto min-h-10 w-full justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] data-[active=true]:bg-accent data-[active=true]:hover:bg-accent dark:data-[active=true]:hover:bg-accent data-[active=true]:text-accent-foreground shell:text-sm"
|
||||
: "group/company-section cursor-pointer text-foreground hover:text-primary";
|
||||
const peopleFiltered = searchParams.get("contactsOnly") === "true" || searchParams.has("active");
|
||||
|
||||
return (
|
||||
<nav
|
||||
@ -36,65 +48,76 @@ export function CompanySectionNavigation({
|
||||
: "flex flex-wrap gap-2"
|
||||
}
|
||||
>
|
||||
{entries.map(([section, label, Icon]) => {
|
||||
const count = counts[section];
|
||||
<SidebarMenu className={layout === "inline" ? "flex-row flex-wrap gap-2" : undefined}>
|
||||
{entries.map(([section, label, Icon]) => {
|
||||
const count = counts[section];
|
||||
|
||||
const itemClassName =
|
||||
layout === "sidebar"
|
||||
? "group/company-section cursor-pointer text-foreground hover:text-primary h-auto min-h-10 w-full justify-start whitespace-normal text-left text-[13px] leading-[1.4285714286] data-[active=true]:bg-accent data-[active=true]:hover:bg-accent dark:data-[active=true]:hover:bg-accent data-[active=true]:text-accent-foreground shell:text-sm"
|
||||
: "group/company-section cursor-pointer text-foreground hover:text-primary";
|
||||
const content = (
|
||||
<>
|
||||
{" "}
|
||||
<Icon aria-hidden="true" className="size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1 text-left">{label}</span>
|
||||
{count && (
|
||||
<CountBadge
|
||||
className="ms-auto group-data-[active=true]/company-section:bg-primary/10 group-data-[active=true]/company-section:text-primary"
|
||||
count={count.count}
|
||||
errorLabel={t("navigationCounts.error")}
|
||||
failed={count.failed}
|
||||
loadingLabel={t("navigationCounts.loading")}
|
||||
locale={i18n.language}
|
||||
/>
|
||||
)}{" "}
|
||||
</>
|
||||
);
|
||||
const isPeople = section === "people";
|
||||
const showPeople = isPeople && peopleExpanded;
|
||||
const active = activeSection === section;
|
||||
const filteredParent = isPeople && peopleFiltered;
|
||||
const currentPage = active && !filteredParent ? "page" : undefined;
|
||||
const containerClassName = ["people", "inventory"].includes(section)
|
||||
? "basis-full border-t border-border pt-2 mt-2 shell:basis-auto"
|
||||
: undefined;
|
||||
const branchProps = {
|
||||
"aria-expanded": isPeople ? peopleExpanded : undefined,
|
||||
"data-active": active,
|
||||
};
|
||||
const content = (
|
||||
<>
|
||||
{" "}
|
||||
<Icon aria-hidden="true" className="size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1 text-left">{label}</span>
|
||||
{isPeople && <PeopleChevron aria-hidden="true" className="size-4 shrink-0" />}
|
||||
{count && (
|
||||
<CountBadge
|
||||
className="ms-auto group-data-[active=true]/company-section:bg-primary/10 group-data-[active=true]/company-section:text-primary"
|
||||
count={count.count}
|
||||
errorLabel={t("navigationCounts.error")}
|
||||
failed={count.failed}
|
||||
loadingLabel={t("navigationCounts.loading")}
|
||||
locale={i18n.language}
|
||||
/>
|
||||
)}{" "}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
section === "people" || section === "inventory"
|
||||
? "basis-full border-t border-border pt-2 mt-2 shell:basis-auto"
|
||||
: "contents"
|
||||
}
|
||||
key={section}
|
||||
>
|
||||
{onSectionChange ? (
|
||||
<Button
|
||||
aria-pressed={activeSection === section}
|
||||
className={itemClassName}
|
||||
data-active={activeSection === section}
|
||||
onClick={() => onSectionChange(section)}
|
||||
variant="ghost"
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
) : (
|
||||
<BrowserLink
|
||||
aria-current={activeSection === section ? "page" : undefined}
|
||||
className={`${buttonVariants({ variant: "ghost" })} ${itemClassName}`}
|
||||
data-active={activeSection === section}
|
||||
data-slot="button"
|
||||
href={companySectionHref(companyId, section)}
|
||||
onClick={onNavigate}
|
||||
>
|
||||
{content}
|
||||
</BrowserLink>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<SidebarMenuItem className={containerClassName} key={section}>
|
||||
{onSectionChange ? (
|
||||
<Button
|
||||
{...branchProps}
|
||||
aria-pressed={active}
|
||||
className={itemClassName}
|
||||
onClick={() => onSectionChange(section)}
|
||||
variant="ghost"
|
||||
>
|
||||
{content}
|
||||
</Button>
|
||||
) : (
|
||||
<BrowserLink
|
||||
{...branchProps}
|
||||
aria-current={currentPage}
|
||||
className={`${buttonVariants({ variant: "ghost" })} ${itemClassName}`}
|
||||
data-slot="button"
|
||||
href={companySectionHref(companyId, section)}
|
||||
onClick={onNavigate}
|
||||
>
|
||||
{content}
|
||||
</BrowserLink>
|
||||
)}
|
||||
{showPeople && (
|
||||
<CompanyPeopleNavigation
|
||||
companyId={companyId}
|
||||
onNavigate={onNavigate}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { assetsEnglish, assetsSpanish } from "@metis/assets/client";
|
||||
import {
|
||||
catalogKeys as assetCatalogKeys,
|
||||
inventoryKeys as assetInventoryKeys,
|
||||
assetsEnglish,
|
||||
assetsSpanish,
|
||||
infrastructureKeys,
|
||||
} from "@metis/assets/client";
|
||||
import { resourcesEnglish, resourcesSpanish } from "@metis/resources/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createInstance } from "i18next";
|
||||
@ -18,12 +24,55 @@ import { SiteSection } from "../../sites/list";
|
||||
import { CatalogOverview } from "../../workspace/catalogs/ui/catalog-overview";
|
||||
import { CompanyDetail } from "../detail";
|
||||
import { CompanyOverview } from "../detail/ui/company-overview";
|
||||
import { CompanyPeopleNavigation } from "../detail/ui/company-people-navigation";
|
||||
import { CompaniesPage } from "../list";
|
||||
|
||||
import { companyKeys } from "./queries";
|
||||
|
||||
const companyId = "e0420eb9-2aab-4368-96a0-2ce83dd23eb4";
|
||||
|
||||
test("las subentradas de Personas conservan contexto y reemplazan el filtro de la vista", async () => {
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await renderPage(
|
||||
language,
|
||||
<CompanyPeopleNavigation
|
||||
companyId={companyId}
|
||||
searchParams={
|
||||
new URLSearchParams(
|
||||
"active=false&contactsOnly=true&q=Ana&sort=name&direction=desc&page=3"
|
||||
)
|
||||
}
|
||||
/>,
|
||||
() => undefined
|
||||
);
|
||||
const links = Array.from(
|
||||
html.matchAll(/href="([^"]+)"/g),
|
||||
(match) => new URL((match[1] ?? "").replaceAll("&", "&"), "http://metis.invalid")
|
||||
);
|
||||
|
||||
assert.equal(links.length, 3);
|
||||
for (const link of links) {
|
||||
assert.equal(link.pathname, `/companies/${companyId}/people`);
|
||||
assert.equal(link.searchParams.get("q"), "Ana");
|
||||
assert.equal(link.searchParams.get("sort"), "name");
|
||||
assert.equal(link.searchParams.get("direction"), "desc");
|
||||
assert.equal(link.searchParams.has("page"), false);
|
||||
}
|
||||
assert.equal(links[0]?.searchParams.get("contactsOnly"), "true");
|
||||
assert.equal(links[0]?.searchParams.has("active"), false);
|
||||
assert.equal(links[1]?.searchParams.get("active"), "true");
|
||||
assert.equal(links[1]?.searchParams.has("contactsOnly"), false);
|
||||
assert.equal(links[2]?.searchParams.get("active"), "false");
|
||||
assert.equal(links[2]?.searchParams.has("contactsOnly"), false);
|
||||
assert.equal((html.match(/aria-current="page"/g) ?? []).length, 1);
|
||||
const translations = language === "es" ? companiesSpanish : companiesEnglish;
|
||||
|
||||
assert.ok(html.includes(translations.peopleNavigation.contacts));
|
||||
assert.ok(html.includes(translations.peopleNavigation.active));
|
||||
assert.ok(html.includes(translations.peopleNavigation.inactive));
|
||||
}
|
||||
});
|
||||
|
||||
async function renderPage(
|
||||
language: string,
|
||||
children: ReactNode,
|
||||
@ -343,6 +392,72 @@ test("el menú de compañía ordena trabajo y personas por grupos sin repetir la
|
||||
assert.ok(html.includes('data-slot="avatar-badge"'));
|
||||
});
|
||||
|
||||
for (const state of ["ready", "pending", "error"] as const) {
|
||||
test(`el menú muestra el contador de infraestructura: ${state}`, async () => {
|
||||
for (const language of ["es", "en"]) {
|
||||
const html = await renderPage(
|
||||
language,
|
||||
<CompanyDetail
|
||||
embedded
|
||||
id={companyId}
|
||||
onBack={() => undefined}
|
||||
onSectionChange={() => undefined}
|
||||
/>,
|
||||
(client) => {
|
||||
client.setQueryData(companyKeys.detail(companyId), {
|
||||
company: {
|
||||
id: companyId,
|
||||
name: "Empresa",
|
||||
active: true,
|
||||
people: [],
|
||||
contacts: [],
|
||||
departments: [],
|
||||
assignments: [],
|
||||
sites: [],
|
||||
},
|
||||
});
|
||||
client.setQueryData(assetInventoryKeys.company(companyId), {
|
||||
assets: [],
|
||||
workstations: [],
|
||||
relationships: [],
|
||||
});
|
||||
client.setQueryData(assetCatalogKeys.scope(companyId), { items: [] });
|
||||
if (state === "ready") {
|
||||
client.setQueryData(infrastructureKeys.company(companyId), { items: [] });
|
||||
} else if (state === "error") {
|
||||
client.setQueryDefaults(infrastructureKeys.company(companyId), { retryOnMount: false });
|
||||
client
|
||||
.getQueryCache()
|
||||
.build(client, {
|
||||
queryKey: infrastructureKeys.company(companyId),
|
||||
})
|
||||
.setState({ status: "error", error: new Error("NETWORK_ERROR") });
|
||||
}
|
||||
}
|
||||
);
|
||||
const navigation = html.match(/<nav[^>]*>[\s\S]*?<\/nav>/)?.[0];
|
||||
const entry = navigation?.match(
|
||||
new RegExp(`<a[^>]*href="/companies/${companyId}/infrastructure"[^>]*>[\\s\\S]*?</a>`)
|
||||
)?.[0];
|
||||
|
||||
assert.ok(entry);
|
||||
assert.ok(entry.includes('data-slot="badge"'));
|
||||
assert.ok(entry.includes(state === "ready" ? ">0</span>" : ">—</span>"));
|
||||
if (state !== "ready") {
|
||||
const translations = language === "es" ? companiesSpanish : companiesEnglish;
|
||||
|
||||
assert.ok(
|
||||
entry.includes(
|
||||
state === "error"
|
||||
? translations.navigationCounts.error
|
||||
: translations.navigationCounts.loading
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("sedes: listado traducido con filtro de archivadas y solo sedes activas por defecto", async () => {
|
||||
const company = {
|
||||
id: companyId,
|
||||
|
||||
@ -24,9 +24,15 @@ export function usePeopleListContext(
|
||||
} = useEntityNavigation(`/companies/${companyId}/people`, initialId);
|
||||
const [localSearch, setLocalSearch] = useState("");
|
||||
const [localContactsOnly, setLocalContactsOnly] = useState(false);
|
||||
const [localActive, setLocalActive] = useState<boolean | undefined>(undefined);
|
||||
const { searchParams, setSearchParams } = useBrowserSearchParams();
|
||||
const search = routed ? (searchParams.get("q") ?? "") : localSearch;
|
||||
const contactsOnly = routed ? searchParams.get("contactsOnly") === "true" : localContactsOnly;
|
||||
const active = routed
|
||||
? searchParams.has("active")
|
||||
? searchParams.get("active") === "true"
|
||||
: undefined
|
||||
: localActive;
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const navigation = useListLocation(
|
||||
{ sort: "sort", direction: "direction", page: "page" },
|
||||
@ -75,6 +81,36 @@ export function usePeopleListContext(
|
||||
});
|
||||
}
|
||||
|
||||
function setActive(value: boolean | undefined) {
|
||||
if (!routed) {
|
||||
setLocalActive(value);
|
||||
|
||||
return;
|
||||
}
|
||||
setSearchParams((params) => {
|
||||
params.delete("page");
|
||||
if (value === undefined) {
|
||||
params.delete("active");
|
||||
} else {
|
||||
params.set("active", String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
if (!routed) {
|
||||
setLocalContactsOnly(false);
|
||||
setLocalActive(undefined);
|
||||
|
||||
return;
|
||||
}
|
||||
setSearchParams((params) => {
|
||||
params.delete("page");
|
||||
params.delete("contactsOnly");
|
||||
params.delete("active");
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
routed &&
|
||||
@ -112,6 +148,9 @@ export function usePeopleListContext(
|
||||
setSearch,
|
||||
contactsOnly,
|
||||
setContactsOnly,
|
||||
active,
|
||||
setActive,
|
||||
clearFilters,
|
||||
rootRef,
|
||||
navigation,
|
||||
selectPerson,
|
||||
|
||||
@ -26,7 +26,7 @@ test("Personas conserva búsqueda por nombre, contactos, consulta y distinción
|
||||
position: null,
|
||||
email: "exclusive@example.test",
|
||||
phone: null,
|
||||
active: true,
|
||||
active: index !== 3,
|
||||
isContact: index === 0,
|
||||
})
|
||||
),
|
||||
@ -48,19 +48,28 @@ test("Personas conserva búsqueda por nombre, contactos, consulta y distinción
|
||||
lng: language,
|
||||
resources: { es: { companies: companiesSpanish }, en: { companies: companiesEnglish } },
|
||||
});
|
||||
for (const [query, contactsOnly, expected] of [
|
||||
[" alvaro ", false, [0, 1, 2]],
|
||||
["Pena", false, [1]],
|
||||
["PEN\u0303A", false, [0, 2]],
|
||||
["alvaro", true, [0]],
|
||||
["exclusive@example.test", false, []],
|
||||
] satisfies [string, boolean, number[]][]) {
|
||||
for (const [query, contactsOnly, active, expected] of [
|
||||
[" alvaro ", false, undefined, [0, 1, 2]],
|
||||
["Pena", false, undefined, [1]],
|
||||
["PEN\u0303A", false, undefined, [0, 2]],
|
||||
["alvaro", true, undefined, [0]],
|
||||
["exclusive@example.test", false, undefined, []],
|
||||
["", false, true, [0, 1, 2]],
|
||||
["", false, false, [3]],
|
||||
["", true, false, []],
|
||||
["María", false, true, []],
|
||||
] satisfies [string, boolean, boolean | undefined, number[]][]) {
|
||||
const params = new URLSearchParams({ q: query, contactsOnly: String(contactsOnly) });
|
||||
|
||||
if (active !== undefined) {
|
||||
params.set("active", String(active));
|
||||
}
|
||||
Object.defineProperty(globalThis, "window", {
|
||||
configurable: true,
|
||||
value: {
|
||||
location: {
|
||||
pathname: `/companies/${companyId}/people`,
|
||||
search: `?${new URLSearchParams({ q: query, contactsOnly: String(contactsOnly) })}`,
|
||||
search: `?${params.toString()}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -77,7 +86,10 @@ test("Personas conserva búsqueda por nombre, contactos, consulta y distinción
|
||||
);
|
||||
|
||||
company.people.forEach((person, index) => {
|
||||
assert.equal(html.includes(`data-entity-id="${person.id}"`), expected.includes(index));
|
||||
assert.equal(
|
||||
html.includes(`data-entity-id="${person.id}"`),
|
||||
expected.some((value) => value === index)
|
||||
);
|
||||
});
|
||||
assert.ok(html.includes(`value="${query}"`));
|
||||
} finally {
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from "@repo/rdx-ui/components/entity-list";
|
||||
import { FormLabel } from "@repo/rdx-ui/components/form-label";
|
||||
import { PhoneLink } from "@repo/rdx-ui/components/phone-link";
|
||||
import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action";
|
||||
import { EyeIcon, PersonIcon } from "@repo/rdx-ui/lib/icons";
|
||||
import { createTextMatcher } from "@repo/rdx-ui/lib/text-search";
|
||||
import { Button } from "@repo/shadcn-ui/components/button";
|
||||
@ -46,6 +47,9 @@ export function PeopleSection({
|
||||
setSearch,
|
||||
contactsOnly,
|
||||
setContactsOnly,
|
||||
active,
|
||||
setActive,
|
||||
clearFilters,
|
||||
rootRef,
|
||||
navigation,
|
||||
selectPerson,
|
||||
@ -99,7 +103,10 @@ export function PeopleSection({
|
||||
}
|
||||
const matchesSearch = createTextMatcher(search);
|
||||
const people = company.people.filter(
|
||||
(person) => (!contactsOnly || person.isContact) && matchesSearch(person.name)
|
||||
(person) =>
|
||||
(!contactsOnly || person.isContact) &&
|
||||
(active === undefined || person.active === active) &&
|
||||
matchesSearch(person.name)
|
||||
);
|
||||
|
||||
return (
|
||||
@ -277,22 +284,35 @@ export function PeopleSection({
|
||||
},
|
||||
]}
|
||||
emptyMessage={t("noPeople")}
|
||||
filterKey={String(contactsOnly)}
|
||||
filterKey={`${contactsOnly}:${active ?? "all"}`}
|
||||
filters={
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={contactsOnly}
|
||||
id={`${filterId}-contact`}
|
||||
onCheckedChange={(checked) => setContactsOnly(checked === true)}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<SelectWithAction
|
||||
id={`${filterId}-active`}
|
||||
label={t("status")}
|
||||
onValueChange={(value) => setActive(value === "" ? undefined : value === "true")}
|
||||
options={[
|
||||
{ value: "true", label: t("peopleNavigation.active") },
|
||||
{ value: "false", label: t("peopleNavigation.inactive") },
|
||||
]}
|
||||
placeholder={t("peopleNavigation.all")}
|
||||
value={active === undefined ? "" : String(active)}
|
||||
/>
|
||||
<FormLabel htmlFor={`${filterId}-contact`}>{t("contactsOnly")}</FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={contactsOnly}
|
||||
id={`${filterId}-contact`}
|
||||
onCheckedChange={(checked) => setContactsOnly(checked === true)}
|
||||
/>
|
||||
<FormLabel htmlFor={`${filterId}-contact`}>{t("contactsOnly")}</FormLabel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
filtersApplied={contactsOnly}
|
||||
filtersApplied={contactsOnly || active !== undefined}
|
||||
items={people}
|
||||
locale={i18n.language}
|
||||
navigation={routed ? navigation : undefined}
|
||||
onClearFilters={() => setContactsOnly(false)}
|
||||
onClearFilters={clearFilters}
|
||||
onSearchChange={setSearch}
|
||||
search={search}
|
||||
searchPlaceholder={t("searchPerson")}
|
||||
|
||||
@ -159,39 +159,38 @@ export function parseWorkspaceUrl(href: string): WorkspaceLocation {
|
||||
}
|
||||
}
|
||||
|
||||
function globalApplicationQueryMatches(
|
||||
function collectionQueryMatches(
|
||||
location: WorkspaceLocation,
|
||||
query: z.infer<typeof RouteQuerySchema>
|
||||
): boolean {
|
||||
const globalApplications =
|
||||
location.area === "catalogs" &&
|
||||
["global-applications", "global-operating-systems"].includes(location.catalog ?? "");
|
||||
const companyApplications =
|
||||
location.area === "company" && location.section === "resources" && query.template !== undefined;
|
||||
const companySection = location.area === "company" ? location.section : undefined;
|
||||
const companyApplications = companySection === "resources" && query.template !== undefined;
|
||||
const applicationCatalog = globalApplications || companyApplications;
|
||||
|
||||
const activeAllowed = applicationCatalog || companySection === "people";
|
||||
const paginationRequested = query.cursor !== undefined || query.pageSize !== undefined;
|
||||
|
||||
if (
|
||||
(query.active !== undefined || query.cursor !== undefined || query.pageSize !== undefined) &&
|
||||
!applicationCatalog
|
||||
(query.active !== undefined && !activeAllowed) ||
|
||||
(paginationRequested && !applicationCatalog)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
applicationCatalog &&
|
||||
((query.q?.length ?? 0) > 100 ||
|
||||
(query.page !== undefined && !/^[1-9][0-9]{0,5}$/u.test(query.page)))
|
||||
) {
|
||||
return false;
|
||||
if (!applicationCatalog) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return (query.q?.length ?? 0) <= 100 && /^[1-9][0-9]{0,5}$/u.test(query.page ?? "1");
|
||||
}
|
||||
|
||||
function queryMatchesLocation(
|
||||
location: WorkspaceLocation,
|
||||
query: z.infer<typeof RouteQuerySchema>
|
||||
): boolean {
|
||||
if (!globalApplicationQueryMatches(location, query)) {
|
||||
if (!collectionQueryMatches(location, query)) {
|
||||
return false;
|
||||
}
|
||||
if (location.area === "company" && query.companyId && query.companyId !== location.id) {
|
||||
|
||||
@ -12,6 +12,37 @@ const COMPANY_ID = "019b8838-44af-776d-a406-af74452b6f26";
|
||||
const ENTITY_ID = "019b8838-44af-776d-a406-af74452b6f27";
|
||||
const TEMPLATE_ID = "01a1276d-2138-711e-b3d2-99258a02a80f";
|
||||
|
||||
test("Personas admite actividad en listado y ficha, con los demás criterios de contexto", () => {
|
||||
for (const active of ["true", "false"]) {
|
||||
for (const suffix of ["", `/${ENTITY_ID}`]) {
|
||||
assert.deepEqual(
|
||||
parseWorkspaceUrl(
|
||||
`/companies/${COMPANY_ID}/people${suffix}?active=${active}&contactsOnly=true&q=Ana&sort=name&direction=desc&page=2`
|
||||
),
|
||||
{
|
||||
area: "company",
|
||||
id: COMPANY_ID,
|
||||
section: "people",
|
||||
...(suffix ? { entityId: ENTITY_ID } : {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const query of [
|
||||
"active=invalid",
|
||||
"active=true&active=false",
|
||||
"active=true&cursor=abc",
|
||||
"active=false&pageSize=25",
|
||||
]) {
|
||||
assert.deepEqual(parseWorkspaceUrl(`/companies/${COMPANY_ID}/people?${query}`), {
|
||||
area: "invalid-route",
|
||||
});
|
||||
}
|
||||
assert.deepEqual(parseWorkspaceUrl(`/companies/${COMPANY_ID}/assets?active=true`), {
|
||||
area: "invalid-route",
|
||||
});
|
||||
});
|
||||
|
||||
test("el catálogo de aplicaciones de compañía conserva cursor, filtros y retorno a ficha", () => {
|
||||
const query = `template=${TEMPLATE_ID}&q=Ágil&active=false&pageSize=25&page=2&cursor=abc`;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user