|
2 | 2 | import { run } from 'svelte/legacy'; |
3 | 3 |
|
4 | 4 | import { page } from '$app/state'; |
| 5 | + import { browser } from '$app/environment'; |
5 | 6 | import RecursiveTreeView from '$lib/components/TreeView/RecursiveTreeView.svelte'; |
6 | 7 |
|
7 | 8 | import { onMount } from 'svelte'; |
|
39 | 40 | import ConfirmModal from '$lib/components/Modals/ConfirmModal.svelte'; |
40 | 41 | import { displayScoreColor, darkenColor } from '$lib/utils/helpers'; |
41 | 42 | import { auditFiltersStore, expandedNodesState } from '$lib/utils/stores'; |
| 43 | + import { fetchNames, isUuid } from '$lib/utils/related-names'; |
42 | 44 | import { derived } from 'svelte/store'; |
43 | 45 | import { canPerformAction } from '$lib/utils/access-control'; |
44 | 46 | import MarkdownRenderer from '$lib/components/MarkdownRenderer.svelte'; |
|
53 | 55 |
|
54 | 56 | const compliance_assessment = $derived(data.compliance_assessment); |
55 | 57 | const folderId = |
56 | | - typeof compliance_assessment.folder === 'string' |
| 58 | + !compliance_assessment.folder |
| 59 | + ? undefined |
| 60 | + :typeof compliance_assessment.folder === 'string' |
57 | 61 | ? compliance_assessment.folder |
58 | 62 | : compliance_assessment.folder?.id; |
59 | 63 | const framework = data.framework ?? compliance_assessment.framework; |
|
78 | 82 | domain: folderId |
79 | 83 | }); |
80 | 84 |
|
| 85 | + let relatedNames: Record<string, Record<string, string>> = $state({}); |
| 86 | + let relatedNamesKey = ''; |
| 87 | +
|
| 88 | + const getUrlModelForField = (fieldName: string): string | undefined => { |
| 89 | + const modelField = model?.foreignKeyFields?.find((item) => item.field === fieldName); |
| 90 | + return modelField?.urlModel ?? modelField?.field?.replace(/_/g, '-'); |
| 91 | + }; |
| 92 | +
|
| 93 | + const extractId = (value: any): string | null => { |
| 94 | + if (typeof value === 'string' && isUuid(value)) return value; |
| 95 | + if (value && typeof value === 'object' && 'id' in value) { |
| 96 | + const idValue = String(value.id); |
| 97 | + return isUuid(idValue) ? idValue : null; |
| 98 | + } |
| 99 | + return null; |
| 100 | + }; |
| 101 | +
|
| 102 | + const extractIds = (value: any): string[] => { |
| 103 | + if (Array.isArray(value)) { |
| 104 | + return value.map((item) => extractId(item)).filter((item): item is string => Boolean(item)); |
| 105 | + } |
| 106 | + const single = extractId(value); |
| 107 | + return single ? [single] : []; |
| 108 | + }; |
| 109 | +
|
| 110 | + const isIdOnlyArray = (value: any[]): boolean => { |
| 111 | + const hasDisplayLabels = value.some( |
| 112 | + (item) => item && typeof item === 'object' && ('str' in item || 'name' in item) |
| 113 | + ); |
| 114 | + return !hasDisplayLabels && extractIds(value).length > 0; |
| 115 | + }; |
| 116 | +
|
| 117 | + const refreshRelatedNames = async () => { |
| 118 | + if (!browser || !model?.foreignKeyFields) return; |
| 119 | + const idsByModel = new Map<string, Set<string>>(); |
| 120 | + for (const fieldConfig of model.foreignKeyFields) { |
| 121 | + const urlModel = fieldConfig.urlModel ?? fieldConfig.field?.replace(/_/g, '-'); |
| 122 | + if (!urlModel) continue; |
| 123 | + const ids = extractIds(compliance_assessment?.[fieldConfig.field]); |
| 124 | + if (ids.length === 0) continue; |
| 125 | + const bucket = idsByModel.get(urlModel) ?? new Set<string>(); |
| 126 | + ids.forEach((id) => bucket.add(id)); |
| 127 | + idsByModel.set(urlModel, bucket); |
| 128 | + } |
| 129 | + const key = JSON.stringify( |
| 130 | + Array.from(idsByModel.entries()).map(([urlModel, ids]) => [urlModel, Array.from(ids).sort()]) |
| 131 | + ); |
| 132 | + if (!key || key === relatedNamesKey) return; |
| 133 | + relatedNamesKey = key; |
| 134 | +
|
| 135 | + const updates: Record<string, Record<string, string>> = {}; |
| 136 | + for (const [urlModel, ids] of idsByModel.entries()) { |
| 137 | + updates[urlModel] = await fetchNames(urlModel, Array.from(ids)); |
| 138 | + } |
| 139 | + if (Object.keys(updates).length > 0) { |
| 140 | + relatedNames = { ...relatedNames, ...updates }; |
| 141 | + } |
| 142 | + }; |
| 143 | +
|
| 144 | + $effect(() => { |
| 145 | + void refreshRelatedNames(); |
| 146 | + }); |
| 147 | +
|
81 | 148 | const has_threats = data.threats.total_unique_threats > 0; |
82 | 149 |
|
83 | 150 | let threatDialogOpen = $state(false); |
|
484 | 551 | > |
485 | 552 | {#if value} |
486 | 553 | {#if Array.isArray(value)} |
487 | | - <ul> |
488 | | - {#each value as val} |
489 | | - <li> |
490 | | - {#if val.str && val.id} |
491 | | - {@const itemHref = `/${ |
492 | | - URL_MODEL_MAP[data.URLModel]['foreignKeyFields']?.find( |
493 | | - (item) => item.field === key |
494 | | - )?.urlModel |
495 | | - }/${val.id}`} |
496 | | - {#if !page.data.user.is_third_party} |
497 | | - <Anchor href={itemHref} class="anchor">{val.str}</Anchor> |
| 554 | + {@const isIdArray = isIdOnlyArray(value)} |
| 555 | + {@const idValues = isIdArray ? extractIds(value) : []} |
| 556 | + {@const urlModel = isIdArray ? getUrlModelForField(key) : undefined} |
| 557 | + {@const resolvedNames = urlModel ? relatedNames[urlModel] : undefined} |
| 558 | + {@const hasResolvedNames = Boolean(resolvedNames)} |
| 559 | + {@const visibleIds = hasResolvedNames |
| 560 | + ? idValues.filter((id) => resolvedNames?.[id]) |
| 561 | + : []} |
| 562 | + {@const hiddenCount = hasResolvedNames ? idValues.length - visibleIds.length : 0} |
| 563 | + {#if isIdArray} |
| 564 | + {#if hasResolvedNames} |
| 565 | + <ul> |
| 566 | + {#each visibleIds as id} |
| 567 | + {@const label = resolvedNames?.[id]} |
| 568 | + {@const itemHref = label && urlModel ? `/${urlModel}/${id}` : undefined} |
| 569 | + <li> |
| 570 | + {#if label && itemHref} |
| 571 | + {#if !page.data.user.is_third_party} |
| 572 | + <Anchor href={itemHref} class="anchor">{label}</Anchor> |
| 573 | + {:else} |
| 574 | + {label} |
| 575 | + {/if} |
| 576 | + {:else if label} |
| 577 | + {label} |
| 578 | + {/if} |
| 579 | + </li> |
| 580 | + {/each} |
| 581 | + </ul> |
| 582 | + {#if hiddenCount > 0} |
| 583 | + <p class="text-xs text-yellow-700"> |
| 584 | + {m.objectsNotVisible({ |
| 585 | + count: hiddenCount, |
| 586 | + s: hiddenCount === 1 ? '' : 's' |
| 587 | + })} |
| 588 | + </p> |
| 589 | + {/if} |
| 590 | + {:else} |
| 591 | + <ul> |
| 592 | + {#each idValues as id} |
| 593 | + <li>{id}</li> |
| 594 | + {/each} |
| 595 | + </ul> |
| 596 | + {/if} |
| 597 | + {:else} |
| 598 | + <ul> |
| 599 | + {#each value as val} |
| 600 | + <li> |
| 601 | + {#if val?.str && val?.id} |
| 602 | + {@const itemHref = `/${ |
| 603 | + URL_MODEL_MAP[data.URLModel]['foreignKeyFields']?.find( |
| 604 | + (item) => item.field === key |
| 605 | + )?.urlModel |
| 606 | + }/${val.id}`} |
| 607 | + {#if !page.data.user.is_third_party} |
| 608 | + <Anchor href={itemHref} class="anchor">{val.str}</Anchor> |
| 609 | + {:else} |
| 610 | + {val.str} |
| 611 | + {/if} |
| 612 | + {:else if val?.str} |
| 613 | + {safeTranslate(val.str)} |
498 | 614 | {:else} |
499 | | - {val.str} |
| 615 | + {safeTranslate(val)} |
500 | 616 | {/if} |
501 | | - {:else} |
502 | | - {val} |
503 | | - {/if} |
504 | | - </li> |
505 | | - {/each} |
506 | | - </ul> |
| 617 | + </li> |
| 618 | + {/each} |
| 619 | + </ul> |
| 620 | + {/if} |
| 621 | + {:else if typeof value === 'string' && isUuid(value) && getUrlModelForField(key)} |
| 622 | + {@const urlModel = getUrlModelForField(key)} |
| 623 | + {@const resolvedNames = urlModel ? relatedNames[urlModel] : undefined} |
| 624 | + {@const label = resolvedNames?.[value]} |
| 625 | + {@const itemHref = label && urlModel ? `/${urlModel}/${value}` : undefined} |
| 626 | + {#if label && itemHref} |
| 627 | + {#if !page.data.user.is_third_party} |
| 628 | + <Anchor href={itemHref} class="anchor">{label}</Anchor> |
| 629 | + {:else} |
| 630 | + {label} |
| 631 | + {/if} |
| 632 | + {:else if label} |
| 633 | + {label} |
| 634 | + {:else} |
| 635 | + {value} |
| 636 | + {/if} |
507 | 637 | {:else if value.str && value.id} |
508 | 638 | {@const itemHref = `/${ |
509 | 639 | URL_MODEL_MAP['compliance-assessments']['foreignKeyFields']?.find( |
|
0 commit comments