diff --git a/app/api/api/filters.py b/app/api/api/filters.py index 12f82d97b..519d3cd3b 100644 --- a/app/api/api/filters.py +++ b/app/api/api/filters.py @@ -1,6 +1,6 @@ from django.db.models.query_utils import Q from django_filters import rest_framework as django_filters -from mapping.models import ScanReportValue, VisibilityChoices +from mapping.models import ScanReportField, ScanReportValue, VisibilityChoices from rest_framework import filters @@ -150,3 +150,18 @@ class Meta: fields = { "value": ["in", "icontains"], } + + +class ScanReportFieldFilter(django_filters.FilterSet): + """ + Custom filterset for ScanReportField model. + """ + + has_concepts = HasConceptsFilter() + creation_type = CreationTypeFilter() + + class Meta: + model = ScanReportField + fields = { + "name": ["icontains"], + } diff --git a/app/api/api/serializers.py b/app/api/api/serializers.py index ad582fe8d..413f10336 100644 --- a/app/api/api/serializers.py +++ b/app/api/api/serializers.py @@ -825,6 +825,24 @@ class Meta: ] +class ScanReportFieldListSerializerV3(serializers.ModelSerializer): + concepts = ScanReportConceptSerializerV2(many=True, read_only=True) + mapping_recommendations = MappingRecommendationSerializerV3( + many=True, read_only=True + ) + + class Meta: + model = ScanReportField + fields = [ + "id", + "name", + "description_column", + "type_column", + "concepts", + "mapping_recommendations", + ] + + class ScanReportValueViewSerializerV2(serializers.ModelSerializer): class Meta: model = ScanReportValue diff --git a/app/api/api/urls.py b/app/api/api/urls.py index 161124483..c2cddf88e 100644 --- a/app/api/api/urls.py +++ b/app/api/api/urls.py @@ -109,6 +109,11 @@ views.ScanReportConceptDetailV3.as_view(), name="scan-report-concepts-detail", ), + path( + "v3/scanreports//tables//fields/", + views.ScanReportFieldIndexV3.as_view(), + name="scan-report-fields-v3", + ), path(r"user/me/", views.UserDetailView.as_view(), name="currentuser"), path(r"v2/users/", views.UserViewSet.as_view(), name="users-list"), path(r"v2/usersfilter/", views.UserFilterViewSet.as_view(), name="usersfilter"), diff --git a/app/api/api/views.py b/app/api/api/views.py index fccfff97b..11347367a 100644 --- a/app/api/api/views.py +++ b/app/api/api/views.py @@ -58,7 +58,11 @@ from services.storage_service import StorageService from services.worker_service import get_worker_service -from api.filters import ScanReportAccessFilter, ScanReportValueFilter +from api.filters import ( + ScanReportAccessFilter, + ScanReportFieldFilter, + ScanReportValueFilter, +) from api.mixins import ScanReportPermissionMixin from api.paginations import CustomPagination from api.serializers import ( @@ -70,6 +74,7 @@ ScanReportEditSerializer, ScanReportFieldEditSerializer, ScanReportFieldListSerializerV2, + ScanReportFieldListSerializerV3, ScanReportFilesSerializer, ScanReportTableEditSerializer, ScanReportTableListSerializerV2, @@ -821,6 +826,70 @@ def get_serializer_class(self): return super().get_serializer_class() +class ScanReportFieldIndexV3(ScanReportPermissionMixin, GenericAPIView, ListModelMixin): + """ + A view that provides a list of ScanReportField objects associated + with a specific ScanReportTable. Each field is returned with its + nested ``concepts`` and ``mapping_recommendations`` so the client + can render concept tags without follow-up requests. This view + supports filtering (including ``has_concepts`` and + ``creation_type``), ordering, and pagination. + + Attributes: + serializer_class (Serializer): The serializer class used for + serializing the ScanReportField objects (V3 — includes + nested concepts and mapping recommendations). + filterset_class (FilterSet): The filterset used for filtering, + including the ``has_concepts`` and ``creation_type`` + filters. + filter_backends (list): List of filter backends used for + filtering and ordering. + ordering_fields (list): Fields that can be used for ordering + the results. + pagination_class (Pagination): The pagination class used for + paginating the results. + + Methods: + get(request, *args, **kwargs): + Handles GET requests and retrieves the ScanReportTable + object based on the provided table_pk. Returns a list of + ScanReportField objects associated with the table. + get_queryset(): + Returns the queryset of ScanReportField objects filtered by + the associated ScanReportTable, with ``select_related`` and + ``prefetch_related`` applied to avoid N+1 queries when + serializing nested concepts and mapping recommendations. + list(request, *args, **kwargs): + Returns the paginated and serialized list of + ScanReportField objects. Caching is intentionally omitted + (unlike V2) so concept edits are reflected immediately. + """ + + filterset_class = ScanReportFieldFilter + filter_backends = [DjangoFilterBackend, OrderingFilter] + ordering_fields = ["name", "description_column", "type_column"] + pagination_class = CustomPagination + serializer_class = ScanReportFieldListSerializerV3 + + @extend_schema(responses=ScanReportFieldListSerializerV3) + def get(self, request, *args, **kwargs): + self.table = get_object_or_404(ScanReportTable, pk=kwargs["table_pk"]) + return self.list(request, *args, **kwargs) + + def get_queryset(self): + return ( + ScanReportField.objects.filter(scan_report_table=self.table) + .order_by("id") + .select_related("scan_report_table") + .prefetch_related( + "concepts", + "concepts__concept", + "mapping_recommendations", + "mapping_recommendations__concept", + ) + ) + + class ScanReportValueListV2(ScanReportPermissionMixin, GenericAPIView, ListModelMixin): """ A view for listing ScanReportValue objects associated with a diff --git a/app/api/test/test_serializers.py b/app/api/test/test_serializers.py index 26a0ef8ed..d286ca21f 100644 --- a/app/api/test/test_serializers.py +++ b/app/api/test/test_serializers.py @@ -1,6 +1,7 @@ from api.serializers import ( MappingRecommendationSerializerV3, ScanReportEditSerializer, + ScanReportFieldListSerializerV3, ScanReportValueViewSerializerV3, ) from data.models import Concept @@ -14,6 +15,7 @@ MappingRecommendation, Project, ScanReport, + ScanReportConcept, ScanReportField, ScanReportTable, ScanReportValue, @@ -464,3 +466,81 @@ def test_scan_report_value_v3_serializer_includes_recommendations(self): self.assertEqual( recommendation_data["concept"]["concept_name"], self.concept.concept_name ) + + +class TestScanReportFieldSerializerV3(TestCase): + def setUp(self): + self.scan_report = ScanReport.objects.create( + dataset="Test Dataset", + visibility="PUBLIC", + ) + + self.table = ScanReportTable.objects.create( + scan_report=self.scan_report, + name="Test Table", + ) + + self.field = ScanReportField.objects.create( + scan_report_table=self.table, + name="Test Field", + description_column="Test Description", + type_column="string", + ) + + self.concept = Concept.objects.create( + concept_id=12345, + concept_name="Test Concept", + concept_code="TEST123", + domain_id="Test", + vocabulary_id="Test", + concept_class_id="Test", + standard_concept="S", + valid_start_date="2020-01-01", + valid_end_date="2099-12-31", + ) + + field_content_type = ContentType.objects.get_for_model(ScanReportField) + self.recommendation = MappingRecommendation.objects.create( + content_type=field_content_type, + object_id=self.field.id, + concept=self.concept, + score=0.85, + tool_name="test-tool", + tool_version="1.0.0", + ) + self.scan_report_concept = ScanReportConcept.objects.create( + content_type=field_content_type, + object_id=self.field.id, + concept=self.concept, + ) + + def test_scan_report_Fields_v3_serializer_includes_recommendations(self): + """Test that ScanReportFieldListSerializerV3 includes recommendations .""" + serializer = ScanReportFieldListSerializerV3(self.field) + data = serializer.data + + # Check that mapping recommendations are included + self.assertIn("mapping_recommendations", data) + self.assertEqual(len(data["mapping_recommendations"]), 1) + + recommendation_data = data["mapping_recommendations"][0] + self.assertEqual(recommendation_data["id"], self.recommendation.id) + self.assertEqual(recommendation_data["score"], 0.85) + self.assertEqual(recommendation_data["tool_name"], "test-tool") + self.assertEqual(recommendation_data["tool_version"], "1.0.0") + self.assertEqual(recommendation_data["concept"]["concept_id"], 12345) + self.assertEqual(recommendation_data["concept"]["concept_name"], "Test Concept") + + def test_scan_report_Fields_v3_serializer_includes_concepts(self): + """Test that ScanReportFieldListSerializerV3 includes concepts.""" + serializer = ScanReportFieldListSerializerV3(self.field) + data = serializer.data + + # check that concepts are included + self.assertIn("concepts", data) + self.assertEqual(len(data["concepts"]), 1) + + concept_data = data["concepts"][0] + self.assertEqual(concept_data["id"], self.scan_report_concept.id) + self.assertEqual(concept_data["concept"]["concept_id"], 12345) + self.assertEqual(concept_data["concept"]["concept_name"], "Test Concept") diff --git a/app/api/test/test_views.py b/app/api/test/test_views.py index f53fe7dfb..ae134dd12 100644 --- a/app/api/test/test_views.py +++ b/app/api/test/test_views.py @@ -871,3 +871,138 @@ def test_scan_report_value_list_v3_includes_recommendations(self): self.assertEqual( recommendation_data["concept"]["concept_name"], concept.concept_name ) + + +class TestScanReportFieldListViewset(TestCase): + def setUp(self): + # Set up Data Partner + self.data_partner = DataPartner.objects.create(name="Silvan Elves") + + # Set up datasets + self.public_dataset = Dataset.objects.create( + name="The Shire", + visibility=VisibilityChoices.PUBLIC, + data_partner=self.data_partner, + ) + + self.scan_report = ScanReport.objects.create( + dataset="Test Dataset", + visibility=VisibilityChoices.PUBLIC, + parent_dataset=self.public_dataset, + ) + + self.table = ScanReportTable.objects.create( + scan_report=self.scan_report, + name="Test Table", + ) + + self.field = ScanReportField.objects.create( + scan_report_table=self.table, + name="Test Field", + description_column="Test Description", + type_column="string", + ) + + self.concept = Concept.objects.create( + concept_id=12345, + concept_name="Test Concept", + concept_code="TEST123", + domain_id="Test", + vocabulary_id="Test", + concept_class_id="Test", + standard_concept="S", + valid_start_date="2020-01-01", + valid_end_date="2099-12-31", + ) + + field_content_type = ContentType.objects.get_for_model(ScanReportField) + self.recommendation = MappingRecommendation.objects.create( + content_type=field_content_type, + object_id=self.field.id, + concept=self.concept, + score=0.85, + tool_name="test-tool", + tool_version="1.0.0", + ) + self.scan_report_concept = ScanReportConcept.objects.create( + content_type=field_content_type, + object_id=self.field.id, + concept=self.concept, + ) + + self.client = APIClient() + self.url = ( + f"/api/v3/scanreports/{self.scan_report.id}/tables/{self.table.id}/fields/" + ) + + @mock.patch.dict(os.environ, {"AZ_FUNCTION_USER": "az_functions"}) + def test_scan_report_field_list_v3_includes_recommendations(self): + """Test that ScanReportFieldListV3 includes mapping recommendations.""" + + response = self.client.get(self.url) + # Verify response + self.assertEqual(response.status_code, 200) + data = response.json() + + # Check that the field is returned + self.assertEqual(len(data["results"]), 1) + field_data = data["results"][0] + + # Check that mapping recommendations are included + self.assertIn("mapping_recommendations", field_data) + self.assertEqual(len(field_data["mapping_recommendations"]), 1) + + recommendation_data = field_data["mapping_recommendations"][0] + self.assertEqual(recommendation_data["id"], self.recommendation.id) + self.assertEqual(recommendation_data["score"], 0.85) + self.assertEqual(recommendation_data["tool_name"], "test-tool") + self.assertEqual(recommendation_data["tool_version"], "1.0.0") + self.assertEqual( + recommendation_data["concept"]["concept_id"], self.concept.concept_id + ) + self.assertEqual( + recommendation_data["concept"]["concept_name"], self.concept.concept_name + ) + + @mock.patch.dict(os.environ, {"AZ_FUNCTION_USER": "az_functions"}) + def test_scan_report_field_list_v3_includes_concepts(self): + """Test that ScanReportFieldListV3 includes concepts.""" + + response = self.client.get(self.url) + # Verify response + self.assertEqual(response.status_code, 200) + data = response.json() + + # Check that the field is returned + self.assertEqual(len(data["results"]), 1) + field_data = data["results"][0] + # Check that concepts are included + self.assertIn("concepts", field_data) + self.assertEqual(len(field_data["concepts"]), 1) + + concept_data = field_data["concepts"][0] + self.assertEqual(concept_data["id"], self.scan_report_concept.id) + self.assertEqual(concept_data["concept"]["concept_id"], 12345) + self.assertEqual(concept_data["concept"]["concept_name"], "Test Concept") + self.assertEqual(concept_data["concept"]["concept_code"], "TEST123") + + @mock.patch.dict(os.environ, {"AZ_FUNCTION_USER": "az_functions"}) + def test_scan_report_field_list_v3_filter_has_concepts_false(self): + """Test that ?has_concepts=false returns only fields with no concepts.""" + # Add a second field with no concepts attached + unmapped_field = ScanReportField.objects.create( + scan_report_table=self.table, + name="Unmapped Field", + description_column="No concepts here", + type_column="string", + ) + + response = self.client.get(f"{self.url}?has_concepts=false") + + self.assertEqual(response.status_code, 200) + data = response.json() + + # Only the unmapped field should be returned; the mapped one from setUp + # has a ScanReportConcept attached and should be filtered out. + self.assertEqual(len(data["results"]), 1) + self.assertEqual(data["results"][0]["id"], unmapped_field.id) diff --git a/app/next-client-app/api/scanreports.ts b/app/next-client-app/api/scanreports.ts index 8ac524bf3..682a3623f 100644 --- a/app/next-client-app/api/scanreports.ts +++ b/app/next-client-app/api/scanreports.ts @@ -24,6 +24,9 @@ const fetchKeys = { ) => `v2/scanreports/${scanReportId}/tables/${tableId}/fields/${fieldId}/`, fields: (scanReportId: string, tableId: string, filter?: string) => `v2/scanreports/${scanReportId}/tables/${tableId}/fields/?${filter}`, + + fieldsV3: (scanReportId: string, tableId: string, filter?: string) => + `v3/scanreports/${scanReportId}/tables/${tableId}/fields/?${filter}`, values: ( scanReportId: string, tableId: string, @@ -315,3 +318,19 @@ export async function getScanReportValuesV3( return { count: 0, next: null, previous: null, results: [] }; } } + +export async function getScanReportFieldsV3( + scanReportId: string, + tableId: string, + filter: string | undefined, +): Promise> { + try { + return await request>( + fetchKeys.fieldsV3(scanReportId, tableId, filter), + ); + } catch (error) { + console.warn("Failed to fetch data."); + return { count: 0, next: null, previous: null, results: [] }; + } +} + diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/columns.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/columns.tsx index 94b727494..b685a986a 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/columns.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/columns.tsx @@ -2,8 +2,6 @@ import { ColumnDef } from "@tanstack/react-table"; import { DataTableColumnHeader } from "@/components/data-table/DataTableColumnHeader"; -import { ConceptTags } from "@/components/concepts/concept-tags"; -import AddConcept from "@/components/concepts/add-concept"; import { EditButton } from "@/components/scanreports/EditButton"; import { Suspense } from "react"; import { Skeleton } from "@/components/ui/skeleton"; @@ -14,12 +12,15 @@ import CopyButton from "@/components/core/CopyButton"; import { enableAIRecommendation } from "@/constants"; import { Tooltips } from "@/components/core/Tooltips"; import { AISuggestionsButton } from "@/components/recommendations/ai-suggesions-button"; +import { ConceptTagsV3 } from "@/components/concepts/ConceptTagsV3"; +import AddConceptV3 from "@/components/concepts/AddConceptV3"; export const columns = ( - addSR: (concept: ScanReportConcept, c: Concept) => void, - deleteSR: (id: number) => void -): ColumnDef[] => { - const baseColumns: ColumnDef[] = [ + tableId: string, + canEdit: boolean, + scanReportId: string, +): ColumnDef[] => { + const baseColumns: ColumnDef[] = [ { id: "Name", header: ({ column }) => ( @@ -93,7 +94,13 @@ export const columns = ( // Just in case the concepts tags need more time to load some data // --> showing skeleton having same width with the concept tag area }> - + ); }, @@ -102,16 +109,14 @@ export const columns = ( id: "Add Concept", header: "", cell: ({ row }) => { - const { scan_report_table, id, permissions } = row.original; - const canEdit = - permissions.includes("CanEdit") || permissions.includes("CanAdmin"); return ( - ); }, @@ -131,11 +136,11 @@ export const columns = ( enableHiding: true, enableSorting: false, cell: ({ row }) => { - const { name, id, scan_report_table } = row.original; + const { name, id } = row.original; return ( @@ -147,7 +152,7 @@ export const columns = ( id: "edit", header: "", cell: ({ row }) => { - const { id, permissions } = row.original; + const { id } = row.original; const path = usePathname(); return ( @@ -155,7 +160,7 @@ export const columns = ( prePath={path} fieldID={id} type="field" - permissions={permissions} + permissions={canEdit ? ["CanEdit"] : []} /> ); }, diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/columns.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/columns.tsx index 2b3736d4c..46f6183b6 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/columns.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/columns.tsx @@ -1,22 +1,24 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; import { DataTableColumnHeader } from "@/components/data-table/DataTableColumnHeader"; -import { ConceptTags } from "@/components/concepts/concept-tags"; -import AddConcept from "@/components/concepts/add-concept"; +import { ConceptTagsV3 } from "@/components/concepts/ConceptTagsV3"; import { Suspense } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import CopyButton from "@/components/core/CopyButton"; +import AddConceptV3 from "@/components/concepts/AddConceptV3"; import { AISuggestionsButton } from "@/components/recommendations/ai-suggesions-button"; -import { Tooltips } from "@/components/core/Tooltips"; -import { enableAIRecommendation } from "@/constants"; +import { StoredRecommendationsButton } from "@/components/recommendations/stored-recommendations-button"; +import { + enableAIRecommendation, + enableStoredRecommendation, +} from "@/constants"; -// All Standard Columns export const columns = ( - addSR: (concept: ScanReportConcept, c: Concept) => void, - deleteSR: (id: number) => void, - tableId: string -): ColumnDef[] => { - const baseColumns: ColumnDef[] = [ + tableId: string, + canEdit: boolean, + scanReportId: string, +): ColumnDef[] => { + const baseColumns: ColumnDef[] = [ { id: "Value", accessorKey: "value", @@ -24,9 +26,9 @@ export const columns = ( ), enableHiding: true, - enableSorting: false, + enableSorting: true, cell: ({ row }) => { - const { value } = row.original; + const { value, id } = row.original; return (
@@ -53,11 +55,11 @@ export const columns = ( /> ), enableHiding: true, - enableSorting: false, + enableSorting: true, cell: ({ row }) => { const { value_description } = row.original; return ( - + {value_description} ); @@ -71,11 +73,10 @@ export const columns = ( column={column} title="Frequency" sortName="frequency" - className="tabular-nums" /> ), enableHiding: true, - enableSorting: false, + enableSorting: true, cell: ({ row }) => { const { frequency } = row.original; return {frequency}; @@ -92,7 +93,13 @@ export const columns = ( const { concepts } = row.original; return ( }> - + ); }, @@ -101,43 +108,72 @@ export const columns = ( id: "Add Concept", header: "", cell: ({ row }) => { - const { id, permissions } = row.original; - const canEdit = - permissions.includes("CanEdit") || permissions.includes("CanAdmin"); + const { id } = row.original; + return ( - ); }, }, ]; - // AI Suggestions Column & setting as 4th Column + // Stored Recommendations Column - insert at position 1 (2nd column) + if (enableStoredRecommendation === "true") { + baseColumns.splice(1, 0, { + id: "Stored Recommendations", + header: ({ column }) => ( + + ), + enableHiding: true, + enableSorting: false, + cell: ({ row }) => { + const { value, id, mapping_recommendations } = row.original; + + return ( +
+ +
+ ); + }, + }); + } + + // AI Suggestions Column - insert at position 2 (3rd column) if (enableAIRecommendation === "true") { - baseColumns.splice(3, 0, { + baseColumns.splice(2, 0, { id: "AI Suggestions", header: ({ column }) => ( -
- - -
+ ), enableHiding: true, enableSorting: false, cell: ({ row }) => { const { value, id } = row.original; + return ( - +
+ +
); }, }); diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx index 5b7717bfb..ac12c63d8 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx @@ -2,19 +2,13 @@ import { getScanReportField, getScanReportPermissions, getScanReportTable, - getScanReportValues, + getScanReportValuesV3, } from "@/api/scanreports"; import { objToQuery } from "@/lib/client-utils"; -import { - getAllConceptsFiltered, - getAllScanReportConcepts, -} from "@/api/concepts"; -import { ConceptDataTable } from "@/components/concepts/ConceptDataTable"; import { columns } from "./columns"; +import { ConceptDataTableV3 } from "@/components/concepts/ConceptDataTableV3"; import { TableBreadcrumbs } from "@/components/scanreports/TableBreadcrumbs"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; -import { Sparkles } from "lucide-react"; +import { ConceptDataFilter } from "@/components/concepts/ConceptDataFilter"; interface ScanReportsValueProps { params: Promise<{ @@ -29,11 +23,7 @@ export default async function ScanReportsValue(props: ScanReportsValueProps) { const searchParams = await props.searchParams; const params = await props.params; - const { - id, - tableId, - fieldId - } = params; + const { id, tableId, fieldId } = params; const defaultPageSize = 20; const defaultParams = { @@ -44,27 +34,19 @@ export default async function ScanReportsValue(props: ScanReportsValueProps) { const permissions = await getScanReportPermissions(id); const table = await getScanReportTable(id, tableId); const field = await getScanReportField(id, tableId, fieldId); - const scanReportsValues = await getScanReportValues( + const scanReportsValues = await getScanReportValuesV3( id, tableId, fieldId, - query + query, ); - const scanReportsConcepts = - scanReportsValues.results.length > 0 - ? await getAllScanReportConcepts( - `object_id__in=${scanReportsValues.results - .map((item) => item.id) - .join(",")}` - ) - : []; - const conceptsFilter = - scanReportsConcepts.length > 0 - ? await getAllConceptsFiltered( - scanReportsConcepts?.map((item) => item.concept).join(",") - ) - : []; + const filter = ; + + const canEdit = + permissions.permissions.includes("CanEdit") || + permissions.permissions.includes("CanAdmin"); + return (
@@ -76,21 +58,17 @@ export default async function ScanReportsValue(props: ScanReportsValueProps) { fieldName={field.name} variant="field" /> -
-
-
diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx index 4104c99d6..bb41614b3 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx @@ -1,6 +1,7 @@ import { columns } from "./columns"; import { getScanReportFields, + getScanReportFieldsV3, getScanReportPermissions, getScanReportTable, } from "@/api/scanreports"; @@ -11,6 +12,8 @@ import { } from "@/api/concepts"; import { ConceptDataTable } from "@/components/concepts/ConceptDataTable"; import { TableBreadcrumbs } from "@/components/scanreports/TableBreadcrumbs"; +import { ConceptDataTableV3 } from "@/components/concepts/ConceptDataTableV3"; +import { ConceptDataFilter } from "@/components/concepts/ConceptDataFilter"; interface ScanReportsFieldProps { params: Promise<{ @@ -24,10 +27,7 @@ export default async function ScanReportsField(props: ScanReportsFieldProps) { const searchParams = await props.searchParams; const params = await props.params; - const { - id, - tableId - } = params; + const { id, tableId } = params; const defaultPageSize = 20; const defaultParams = { @@ -36,24 +36,14 @@ export default async function ScanReportsField(props: ScanReportsFieldProps) { const combinedParams = { ...defaultParams, ...searchParams }; const query = objToQuery(combinedParams); const tableName = await getScanReportTable(id, tableId); - const scanReportsFields = await getScanReportFields(id, tableId, query); + const scanReportsFields = await getScanReportFieldsV3(id, tableId, query); const permissions = await getScanReportPermissions(id); - const scanReportsConcepts = - scanReportsFields.results.length > 0 - ? await getAllScanReportConcepts( - `object_id__in=${scanReportsFields.results - .map((item) => item.id) - .join(",")}`, - ) - : []; - const conceptsFilter = - scanReportsConcepts.length > 0 - ? await getAllConceptsFiltered( - scanReportsConcepts?.map((item) => item.concept).join(","), - ) - : []; + const canEdit = permissions.permissions.includes("CanEdit") || + permissions.permissions.includes("CanAdmin"); + + const filter = ; return (
@@ -64,19 +54,17 @@ export default async function ScanReportsField(props: ScanReportsFieldProps) { variant="table" />
-
); -} +} \ No newline at end of file diff --git a/app/next-client-app/auth/options.ts b/app/next-client-app/auth/options.ts index 897d36af4..a749ba146 100644 --- a/app/next-client-app/auth/options.ts +++ b/app/next-client-app/auth/options.ts @@ -11,7 +11,7 @@ const getCurrentEpochTime = () => { }; export const options: NextAuthOptions = { - secret: process.env.AUTH_SECRET, + secret: process.env.NEXTAUTH_SECRET, session: { strategy: "jwt", maxAge: BACKEND_REFRESH_TOKEN_LIFETIME, diff --git a/app/next-client-app/components/concepts/AddConceptV3.tsx b/app/next-client-app/components/concepts/AddConceptV3.tsx index 9271488ef..b99183121 100644 --- a/app/next-client-app/components/concepts/AddConceptV3.tsx +++ b/app/next-client-app/components/concepts/AddConceptV3.tsx @@ -32,7 +32,7 @@ export default function AddConceptV3({ creation_type: "M", table_id: tableId, }, - `/scanreports/${scanReportId}/tables/${tableId}/fields/${fieldId}/beta` + `/scanreports/${scanReportId}/tables/${tableId}/fields/${fieldId}` ); if (response) { diff --git a/app/next-client-app/components/concepts/ConceptDataFilter.tsx b/app/next-client-app/components/concepts/ConceptDataFilter.tsx index 2d14da28c..8c5532b99 100644 --- a/app/next-client-app/components/concepts/ConceptDataFilter.tsx +++ b/app/next-client-app/components/concepts/ConceptDataFilter.tsx @@ -5,6 +5,8 @@ import { navigateWithSearchParam } from "@/lib/client-utils"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { DataTableFilter } from "../data-table/DataTableFilter"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; const ConceptDataOptions = [ { @@ -24,7 +26,7 @@ const ConceptDataOptions = [ }, ] -export function ConceptDataFilter() { +export function ConceptDataFilter({ showUnmappedFilter = false }: { showUnmappedFilter?: boolean }) { const router = useRouter(); const searchParam = useSearchParams(); @@ -80,8 +82,19 @@ export function ConceptDataFilter() { ); }; + const unmappedOnly = searchParam.get("has_concepts") === "false"; + + const handleUnmappedToggle = (checked: boolean) => { + navigateWithSearchParam( + "has_concepts", + checked ? "false" : "", + router, + searchParam + ); + }; + return ( -
+
(setOptions([]), handleFacetsFilter())} /> + {showUnmappedFilter && ( +
+ handleUnmappedToggle(checked === true)} + /> + +
+ )}
); } diff --git a/app/next-client-app/components/concepts/ConceptDataTableV3.tsx b/app/next-client-app/components/concepts/ConceptDataTableV3.tsx index 3c73771ca..3c7af4c24 100644 --- a/app/next-client-app/components/concepts/ConceptDataTableV3.tsx +++ b/app/next-client-app/components/concepts/ConceptDataTableV3.tsx @@ -3,7 +3,7 @@ import { DataTable } from "@/components/data-table"; interface CustomDataTableProps { - scanReportsData: ScanReportValueV3[]; + scanReportsData: T[]; canEdit: boolean; count: number; defaultPageSize: 10 | 20 | 30 | 40 | 50; diff --git a/app/next-client-app/components/concepts/ConceptTagsV3.tsx b/app/next-client-app/components/concepts/ConceptTagsV3.tsx index fa499b80e..9baf56126 100644 --- a/app/next-client-app/components/concepts/ConceptTagsV3.tsx +++ b/app/next-client-app/components/concepts/ConceptTagsV3.tsx @@ -30,7 +30,7 @@ export function ConceptTagsV3({ setOptimisticConcepts(conceptId); await deleteConceptV3( conceptId, - `/scanreports/${scanReportId}/tables/${tableId}/fields/${fieldId}/beta` + `/scanreports/${scanReportId}/tables/${tableId}/fields/${fieldId}` ); toast.success("Concept Id Deleted"); } catch (error) { diff --git a/app/next-client-app/components/form-components/FormikSelect.tsx b/app/next-client-app/components/form-components/FormikSelect.tsx index c528c2e4f..50bdc5a1c 100644 --- a/app/next-client-app/components/form-components/FormikSelect.tsx +++ b/app/next-client-app/components/form-components/FormikSelect.tsx @@ -1,4 +1,5 @@ import { Field, FieldInputProps, FieldProps, FormikProps } from "formik"; +import { useId } from "react"; import Select from "react-select"; import makeAnimated from "react-select/animated"; import config from "@/tailwind.config"; @@ -26,6 +27,7 @@ const CustomSelect = ({ isDisabled: boolean; required?: boolean; }) => { + const instanceId = useId(); const animatedComponents = makeAnimated(); // Optional: detect dark mode for better contrast @@ -53,6 +55,7 @@ const CustomSelect = ({ return ( ; isDisabled: boolean; }) => { + const instanceId = useId(); const animatedComponents = makeAnimated(); const onChange = (newValue: any, actionMeta: any) => { const selectedValues = isMulti @@ -68,6 +69,7 @@ const CustomSelect = ({ return (