diff --git a/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx b/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx index 8335f4ad..2af6bb93 100644 --- a/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx +++ b/app/[locale]/(user)/aimodels/[modelId]/AIModelDetailsPage.tsx @@ -12,7 +12,7 @@ import Metadata from './components/Metadata'; import PrimaryData from './components/PrimaryData'; import Versions from './components/Versions'; -const aiModelQuery: any = graphql(` +const aiModelQuery = graphql(` query getAIModel($modelId: Int!) { getAiModel(modelId: $modelId) { id @@ -102,7 +102,7 @@ export default function AIModelDetailsPage({ } ); - const modelData = (data as any)?.getAiModel; + const modelData = data?.getAiModel; const jsonLd = generateJsonLd({ '@context': 'https://schema.org', diff --git a/app/[locale]/(user)/aimodels/[modelId]/components/Details/index.tsx b/app/[locale]/(user)/aimodels/[modelId]/components/Details/index.tsx index 44e50297..6518e48c 100644 --- a/app/[locale]/(user)/aimodels/[modelId]/components/Details/index.tsx +++ b/app/[locale]/(user)/aimodels/[modelId]/components/Details/index.tsx @@ -2,8 +2,25 @@ import { Text } from 'opub-ui'; +interface ModelEndpoint { + id?: string | number; + url?: string; + isPrimary?: boolean; + httpMethod?: string; + authType?: string; + timeoutSeconds?: number; + isActive?: boolean; +} + +interface DetailsData { + inputSchema?: Record | null; + outputSchema?: Record | null; + metadata?: Record | null; + endpoints?: ModelEndpoint[] | null; +} + interface DetailsProps { - data: any; + data: DetailsData; } export default function Details({ data }: DetailsProps) { @@ -71,7 +88,7 @@ export default function Details({ data }: DetailsProps) { API Endpoints
- {data.endpoints.map((endpoint: any, index: number) => ( + {data.endpoints.map((endpoint, index) => (
| null; + geographies?: Array<{ name: string }> | null; +} + interface MetadataProps { - data: any; + data: MetadataData; } export default function Metadata({ data }: MetadataProps) { @@ -67,9 +99,9 @@ export default function Metadata({ data }: MetadataProps) { // Get primary version info const primaryVersion = - data.versions?.find((v: any) => v.isLatest) || data.versions?.[0]; + data.versions?.find((v) => v.isLatest) || data.versions?.[0]; const primaryProvider = - primaryVersion?.providers?.find((p: any) => p.isPrimary) || + primaryVersion?.providers?.find((p) => p.isPrimary) || primaryVersion?.providers?.[0]; const providerLabels: Record = { @@ -212,7 +244,7 @@ export default function Metadata({ data }: MetadataProps) { Sector
- {data.sectors.map((sector: any, index: number) => ( + {data.sectors.map((sector, index) => (
- {data.geographies.map((geo: any, index: number) => ( + {data.geographies.map((geo, index) => ( | null; + metadata?: { + keyFeatures?: string[]; + } | null; +} + interface PrimaryDataProps { - data: any; + data: PrimaryDataModel; isLoading: boolean; } @@ -17,7 +27,7 @@ export default function PrimaryData({ data, isLoading }: PrimaryDataProps) { {data.tags && data.tags.length > 0 && (
- {data.tags.map((tag: any) => ( + {data.tags.map((tag) => ( ( + cell: ({ row }: VersionCellProps) => ( {row.original.lifecycleStage?.replace(/_/g, ' ') || 'Development'} @@ -44,12 +82,12 @@ export default function Versions({ data }: VersionsProps) { { accessorKey: 'providers', header: 'Access Methods', - cell: ({ row }: any) => { + cell: ({ row }: VersionCellProps) => { const providers = row.original.providers || []; if (providers.length === 0) return N/A; return (
- {providers.map((p: any) => ( + {providers.map((p) => ( {providerLabels[p.provider] || p.provider} @@ -61,7 +99,7 @@ export default function Versions({ data }: VersionsProps) { { accessorKey: 'maxTokens', header: 'Max Tokens', - cell: ({ row }: any) => ( + cell: ({ row }: VersionCellProps) => ( {row.original.maxTokens?.toLocaleString() || 'N/A'} @@ -70,7 +108,7 @@ export default function Versions({ data }: VersionsProps) { { accessorKey: 'updatedAt', header: 'Last Updated', - cell: ({ row }: any) => ( + cell: ({ row }: VersionCellProps) => ( {formatDate(row.original.updatedAt || row.original.createdAt) || ''} @@ -79,7 +117,7 @@ export default function Versions({ data }: VersionsProps) { ]; }; - const generateTableData = (version: any) => { + const generateTableData = (version: ModelVersion) => { return [ { lifecycleStage: version.lifecycleStage, @@ -100,7 +138,7 @@ export default function Versions({ data }: VersionsProps) {
- {data.versions.map((version: any) => ( + {data.versions.map((version) => (
- {version.providers.map((provider: any) => ( + {version.providers.map((provider) => (
{ x: number; y: number }; +} + export function Content({ bar, line, @@ -22,7 +36,7 @@ export function Content({ bar: Props; line: Props; stacked: Props; - mapOptions: any; + mapOptions: MapChartOptions; }) { const mapDataFn = (value: number) => { return value >= 330 @@ -124,7 +138,7 @@ const ChartMap = ({ options, props, }: { - options: any; + options: MapChartOptions & { mapDataFn: (value: number) => string }; props: { title: string; }; @@ -132,12 +146,16 @@ const ChartMap = ({ const [svgURL, setSvgURL] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); const ref = React.useRef(null); - const [map, setMap] = React.useState(null); + const [map, setMap] = React.useState(null); const isDesktop = useMediaQuery('(min-width: 768px)'); const { createSvg, svgToPngURL, downloadFile, domToUrl } = useScreenshot(); async function generateImage() { + if (!map) { + return; + } + setIsLoading(true); const targetElm = ref.current?.querySelector('.leaflet-map-pane'); @@ -218,7 +236,14 @@ const Template = ({ {title}

{data ? ( - SVG + SVG ) : ( 'Loading...' )} diff --git a/app/[locale]/(user)/chart/page.tsx b/app/[locale]/(user)/chart/page.tsx index d92b7d37..284f8d94 100644 --- a/app/[locale]/(user)/chart/page.tsx +++ b/app/[locale]/(user)/chart/page.tsx @@ -4,11 +4,12 @@ import { barOptions, lineOptions, stackedOptions } from './chart'; import { Content } from './Content'; export default async function Home() { + const mapCenter: [number, number] = [26.193, 92.3]; const mapOptions = { mapProperty: 'dt_code', mapZoom: 7.9, fillOpacity: 1, - mapCenter: [26.193, 92.3], + mapCenter, features: json.features, }; diff --git a/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx b/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx index 5eca122a..5f59ef8b 100644 --- a/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx +++ b/app/[locale]/(user)/collaboratives/CollaborativesListingClient.tsx @@ -1,10 +1,9 @@ 'use client'; -import { ComponentType, useState } from 'react'; +import { useState } from 'react'; import Image from 'next/image'; import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; import { graphql } from '@/gql'; -import { TypeCollaborative } from '@/gql/generated/graphql'; import { useQuery } from '@tanstack/react-query'; import { Button, Card, Icon, SearchInput, Select, Text } from 'opub-ui'; @@ -93,15 +92,13 @@ const CollaborativesListingClient = () => { data: collaborativesData, isLoading, error, - } = useQuery<{ publishedCollaboratives: TypeCollaborative[] }>( + } = useQuery( ['fetch_published_collaboratives'], async () => { console.log('Fetching collaboratives...'); try { - // @ts-expect-error - Query has no variables - const result = await GraphQLPublic(PublishedCollaboratives as any, {}); - // console.log('Collaboratives result:', result); - return result as { publishedCollaboratives: TypeCollaborative[] }; + const result = await GraphQLPublic(PublishedCollaboratives, {}); + return result; } catch (err) { console.error('Error fetching collaboratives:', err); throw err; @@ -284,7 +281,7 @@ const CollaborativesListingClient = () => { value: 'datasetCount_desc', }, ]} - onChange={(e: any) => { + onChange={(e) => { setSortBy(e); setCurrentPage(1); }} @@ -325,7 +322,7 @@ const CollaborativesListingClient = () => { view="collapsed" > {paginatedCollaboratives.map( - (collaborative: TypeCollaborative) => ( + (collaborative) => ( { // imageUrl={`${process.env.NEXT_PUBLIC_BACKEND_URL}/${collaborative.logo?.path.replace('/code/files/', '')}`} metadataContent={[ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Started', value: formatDate(collaborative.startedOn) || '', stroke: 1.2, }, { - icon: Icons.dataset as any, + icon: Icons.dataset, label: 'Datasets', value: collaborative.datasetCount?.toString() || '0', }, { - icon: Icons.worldPin as ComponentType, + icon: Icons.worldPin, label: 'Geography', value: collaborative.geographies && collaborative.geographies.length > 0 ? collaborative.geographies - .map((geo: any) => geo.name) + .map((geo) => geo.name) .join(', ') : 'N/A', stroke: 1.2, diff --git a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx index 163774d4..a05e3d3b 100644 --- a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx +++ b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/CollaborativeDetailsClient.tsx @@ -1,15 +1,10 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useSyncExternalStore } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; -import { - TypeCollaborative, - TypeDataset, - TypeUseCase, -} from '@/gql/generated/graphql'; import { useAnalytics } from '@/hooks/use-analytics'; import { useQuery } from '@tanstack/react-query'; import { Card, Text } from 'opub-ui'; @@ -240,38 +235,31 @@ const getPlatformEntityUrl = ( const CollaborativeDetailClient = () => { const params = useParams(); const { trackCollaborative } = useAnalytics(); - const locale = - typeof (params as any)?.locale === 'string' - ? (params as any).locale - : undefined; - const [isCollaborativeSubdomainHost, setIsCollaborativeSubdomainHost] = - useState(false); - - useEffect(() => { - if (typeof window === 'undefined') return; - setIsCollaborativeSubdomainHost( - isCollaborativeSubdomainHostname(window.location.hostname) - ); - }, []); + const locale = typeof params.locale === 'string' ? params.locale : undefined; + const collaborativeSlug = + typeof params.collaborativeSlug === 'string' + ? params.collaborativeSlug + : ''; + const isCollaborativeSubdomainHost = useSyncExternalStore( + () => () => {}, + () => isCollaborativeSubdomainHostname(window.location.hostname), + () => false + ); const { data: CollaborativeDetailsData, isLoading, error, - } = useQuery<{ collaborativeBySlug: TypeCollaborative }>( + } = useQuery( [`fetch_CollaborativeDetails_${params.collaborativeSlug}`], async () => { console.log( 'Fetching collaborative details for:', params.collaborativeSlug ); - const result = (await GraphQLPublic( - CollaborativeDetails as any, - {}, - { - slug: params.collaborativeSlug, - } - )) as { collaborativeBySlug: TypeCollaborative }; + const result = await GraphQLPublic(CollaborativeDetails, {}, { + slug: collaborativeSlug, + }); return result; }, { @@ -322,7 +310,11 @@ const CollaborativeDetailClient = () => { }, }); - const organizationPublisherHref = (org: any) => { + const organizationPublisherHref = (org: { + id: string; + name: string; + slug?: string | null; + }) => { const path = `/publishers/organization/${org.slug || org.name}_${org.id}`; // Original: `/publishers/organization/${org.slug + '_' + org.id}`; // Match getPlatformEntityUrl() behavior (absolute to platform host + locale) @@ -348,8 +340,9 @@ const CollaborativeDetailClient = () => { Error Loading Collaborative - {(error as any)?.message?.includes('401') || - (error as any)?.message?.includes('403') + {error instanceof Error && + (error.message.includes('401') || + error.message.includes('403')) ? 'You do not have permission to view this collaborative. Please log in or contact the administrator.' : 'Failed to load collaborative details. Please try again later.'} @@ -402,7 +395,7 @@ const CollaborativeDetailClient = () => {
{CollaborativeDetailsData?.collaborativeBySlug?.supportingOrganizations?.map( - (org: any) => ( + (org) => ( {
{CollaborativeDetailsData?.collaborativeBySlug?.partnerOrganizations?.map( - (org: any) => ( + (org) => ( {
- {useCases.map((useCase: TypeUseCase) => { + {useCases.map((useCase) => { const image = useCase.isIndividualUsecase ? useCase?.user?.profilePicture ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${useCase.user.profilePicture.url}` @@ -479,29 +472,29 @@ const CollaborativeDetailClient = () => { const Geography = useCase.geographies && useCase.geographies.length > 0 ? useCase.geographies - .map((geo: any) => geo.name) + .map((geo) => geo.name) .join(', ') : null; - const MetadataContent = [ - { - icon: Icons.calendarEvent as any, - label: 'Date', - value: formatDate(useCase.modified) || '', - tooltip: 'Date', - stroke: 1.2, - }, - ]; - - if (Geography) { - MetadataContent.push({ - icon: Icons.worldPin as any, - label: 'Geography', - value: Geography, - tooltip: 'Geography', - stroke: 1.2, - }); - } + const dateMeta = { + icon: Icons.calendarEvent, + label: 'Date', + value: formatDate(useCase.modified) || '', + tooltip: 'Date', + stroke: 1.2, + }; + const MetadataContent = Geography + ? ([ + dateMeta, + { + icon: Icons.worldPin, + label: 'Geography', + value: Geography, + tooltip: 'Geography', + stroke: 1.2, + }, + ] as const) + : ([dateMeta] as const); const LeftFooterChips = [ { @@ -526,8 +519,8 @@ const CollaborativeDetailClient = () => { const commonProps = { title: useCase.title || '', description: stripMarkdown(useCase.summary || ''), - metadataContent: MetadataContent as any, - tag: useCase.tags?.map((t: any) => t.value) || [], + metadataContent: MetadataContent, + tag: useCase.tags?.map((t) => t.value) || [], leftFooterChips: LeftFooterChips, rightFooterChips: RightFooterChips, imageUrl: '', @@ -569,7 +562,7 @@ const CollaborativeDetailClient = () => {
{datasets.length > 0 && - datasets.map((dataset: TypeDataset) => ( + datasets.map((dataset) => ( { iconColor={'warning'} metadataContent={[ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', value: formatDate(dataset.modified) || '', stroke: 1.2, }, { - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: dataset.downloadCount.toString(), stroke: 1.2, }, { - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: dataset.geographies && dataset.geographies.length > 0 ? dataset.geographies - .map((geo: any) => geo.name) + .map((geo) => geo.name) .join(', ') : '', stroke: 1.2, diff --git a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx index dfcd425f..0896804b 100644 --- a/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx +++ b/app/[locale]/(user)/collaboratives/[collaborativeSlug]/page.tsx @@ -42,7 +42,7 @@ export async function generateMetadata({ description: Collaborative?.summary || `Explore open data and curated datasets in the ${Collaborative?.title} collaborative.`, - keywords: Collaborative?.tags?.map((tag: any) => tag.value) || [], + keywords: Collaborative?.tags?.map((tag) => tag.value) || [], openGraph: { type: 'article', locale: 'en_US', diff --git a/app/[locale]/(user)/collaboratives/components/Details.tsx b/app/[locale]/(user)/collaboratives/components/Details.tsx index 941411e9..76b025ac 100644 --- a/app/[locale]/(user)/collaboratives/components/Details.tsx +++ b/app/[locale]/(user)/collaboratives/components/Details.tsx @@ -4,11 +4,17 @@ import Image from 'next/image'; import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui'; import { useState } from 'react'; +import { CollaborativeQueryQuery } from '@/gql/generated/graphql'; import { Icons } from '@/components/icons'; import { RichTextRenderer } from '@/components/RichTextRenderer'; import Metadata from './Metadata'; -const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => { +interface PrimaryDetailsProps { + data: CollaborativeQueryQuery; + isLoading: boolean; +} + +const PrimaryDetails = ({ data, isLoading }: PrimaryDetailsProps) => { const [open, setOpen] = useState(false); return ( @@ -19,7 +25,7 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => {
- {data.collaborativeBySlug.tags.map((item: any, index: number) => ( + {data.collaborativeBySlug.tags?.map((item, index: number) => (
{
{data.collaborativeBySlug.title} {
{data.collaborativeBySlug.geographies.map( - (geo: any, index: number) => ( + (geo, index: number) => ( { )}
diff --git a/app/[locale]/(user)/collaboratives/components/Metadata.tsx b/app/[locale]/(user)/collaboratives/components/Metadata.tsx index 687d557b..ec3abcc5 100644 --- a/app/[locale]/(user)/collaboratives/components/Metadata.tsx +++ b/app/[locale]/(user)/collaboratives/components/Metadata.tsx @@ -3,33 +3,45 @@ import Image from 'next/image'; import Link from 'next/link'; import { Button, Icon, Text, Tooltip } from 'opub-ui'; +import { CollaborativeQueryQuery } from '@/gql/generated/graphql'; import { getPlatformRootUrl } from '@/lib/collaborativesRouting'; import { formatDate, getWebsiteTitle } from '@/lib/utils'; import { Icons } from '@/components/icons'; -const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { - const [platformTitle, setPlatformTitle] = useState(null); +interface MetadataProps { + data: CollaborativeQueryQuery; + setOpen?: (isOpen: boolean) => void; +} + +const Metadata = ({ data, setOpen }: MetadataProps) => { + const platformUrl = data.collaborativeBySlug.platformUrl; + const [platformTitle, setPlatformTitle] = useState( + platformUrl === null ? 'N/A' : null + ); + const [prevPlatformUrl, setPrevPlatformUrl] = useState(platformUrl); + if (platformUrl !== prevPlatformUrl) { + setPrevPlatformUrl(platformUrl); + setPlatformTitle(platformUrl === null ? 'N/A' : null); + } useEffect(() => { - const fetchTitle = async () => { - try { - const urlItem = data.collaborativeBySlug.platformUrl; + if (!platformUrl) { + return; + } - if (urlItem && urlItem.value) { - const title = await getWebsiteTitle(urlItem.value); - setPlatformTitle(title); - } - } catch (error) { + let cancelled = false; + getWebsiteTitle(platformUrl) + .then((title) => { + if (!cancelled) setPlatformTitle(title); + }) + .catch((error) => { console.error('Error fetching website title:', error); - } - }; + }); - if (data.collaborativeBySlug.platformUrl === null) { - setPlatformTitle('N/A'); - } else { - fetchTitle(); - } - }, [data.collaborativeBySlug.platformUrl]); + return () => { + cancelled = true; + }; + }, [platformUrl]); const metadata = [ { @@ -40,7 +52,7 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { ) : ( { label: 'Sectors', value: (
- {data.collaborativeBySlug.sectors.length > 0 ? ( + {data.collaborativeBySlug.sectors && + data.collaborativeBySlug.sectors.length > 0 ? ( data.collaborativeBySlug.sectors.map( - (sector: any, index: number) => ( + (sector, index: number) => ( {
{data.collaborativeBySlug.sdgs && data.collaborativeBySlug.sdgs.length > 0 ? ( - data.collaborativeBySlug.sdgs.map((sdg: any, index: number) => ( + data.collaborativeBySlug.sdgs.map((sdg, index: number) => ( { const match = window.location.pathname.match(/^\/([a-z]{2})(\/|$)/i); return match ? `/${match[1].toLowerCase()}` : ''; }; - const contributorHref = (contributor: any) => { + const contributorHref = (contributor: { + id: string; + fullName: string; + }) => { const path = `/publishers/${contributor.fullName}_${contributor.id}`; // Original: `/publishers/${contributor.fullName + '_' + contributor.id}`; // Match getPlatformEntityUrl() behavior (absolute to NEXT_PUBLIC_PLATFORM_URL + locale) @@ -197,7 +213,7 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => {
{data.collaborativeBySlug.contributors.map( - (contributor: any) => ( + (contributor) => ( { // Enable tour for first-time users useTourTrigger(true, 1500); - const Stats: { data: any; isLoading: any } = useQuery([`statsDetails`], () => - GraphQL(statsInfo, {}, []) - ); + const Stats = useQuery([`statsDetails`], () => GraphQL(statsInfo)); const handleSearch = (value: string) => { if (value) { diff --git a/app/[locale]/(user)/components/Datasets.tsx b/app/[locale]/(user)/components/Datasets.tsx index 340b7ad4..46b8d696 100644 --- a/app/[locale]/(user)/components/Datasets.tsx +++ b/app/[locale]/(user)/components/Datasets.tsx @@ -25,19 +25,42 @@ interface Bucket { doc_count: number; } interface Aggregation { - buckets: Bucket[]; + buckets?: Bucket[]; + [key: string]: unknown; } interface Aggregations { [key: string]: Aggregation; } +interface DatasetPublisher { + profile_picture?: string; + logo?: string; +} + +interface DatasetResult { + id: string | number; + title: string; + description?: string; + modified: string; + download_count?: number; + geographies?: Array; + tags?: string[]; + formats?: string[]; + sectors?: string[]; + is_individual_dataset?: boolean; + user?: DatasetPublisher; + organization?: DatasetPublisher; +} + +interface DatasetSearchResponse { + results: DatasetResult[]; + total: number; + aggregations: Aggregations; +} + const Datasets = () => { - const [facets, setFacets] = useState<{ - results: any[]; - total: number; - aggregations: Aggregations; - } | null>(null); + const [facets, setFacets] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchDatasets('?sort=recent&size=5&page=1&sort=recent') @@ -88,11 +111,11 @@ const Datasets = () => { {isLoading ? ( ) : ( - facets?.results?.map((item: any) => { + facets?.results?.map((item) => { const geographies = Array.isArray(item.geographies) && item.geographies.length > 0 ? item.geographies - .map((geo: any) => + .map((geo) => typeof geo === 'string' ? geo : geo?.name ) .filter(Boolean) @@ -109,10 +132,10 @@ const Datasets = () => { {' '} { stroke: 1.2, }, { - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: item.download_count || 0, stroke: 1.2, @@ -144,7 +167,7 @@ const Datasets = () => { formats={item.formats} leftFooterChips={[ { - icon: `/Sectors/${item.sectors[0]}.svg`, + icon: `/Sectors/${item.sectors?.[0]}.svg`, label: 'Sectors', }, ]} diff --git a/app/[locale]/(user)/components/ListingComponent.tsx b/app/[locale]/(user)/components/ListingComponent.tsx index 7b874e21..ebcc25f9 100644 --- a/app/[locale]/(user)/components/ListingComponent.tsx +++ b/app/[locale]/(user)/components/ListingComponent.tsx @@ -5,6 +5,7 @@ import Image from 'next/image'; import { useRouter } from 'next/navigation'; import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; import { fetchData } from '@/fetch'; +import { useMounted } from '@/hooks/use-mounted'; import { useTourTrigger } from '@/hooks/use-tour-trigger'; import { Button, @@ -36,13 +37,63 @@ interface Bucket { } interface Aggregation { - buckets: Bucket[]; + buckets?: Bucket[]; + [key: string]: unknown; } interface Aggregations { [key: string]: Aggregation; } +interface ListingPublisher { + profile_picture?: string; + logo?: string; + name?: string; +} + +interface ListingSdg { + code: string; + name: string; +} + +interface ListingResult { + id: string | number; + title: string; + description?: string; + modified: string; + download_count?: number; + geographies?: string[]; + sdgs?: ListingSdg[]; + has_charts?: boolean; + sectors?: string[]; + tags?: string[]; + formats?: string[]; + logo?: string; + is_individual_dataset?: boolean; + user?: ListingPublisher; + organization?: ListingPublisher; +} + +interface CardMetadataItem { + icon: (typeof Icons)[keyof typeof Icons]; + stroke?: number; + label: string; + value: string | number; + tooltip?: string; +} + +type CardMetadataTuple = + | [] + | [CardMetadataItem] + | [CardMetadataItem, CardMetadataItem] + | [CardMetadataItem, CardMetadataItem, CardMetadataItem]; + +interface FooterChip { + icon: string; + label: string; + tooltip?: string; +} + interface FilterOptions { [key: string]: string[]; } @@ -229,7 +280,7 @@ const ListingComponent: React.FC = ({ useTourTrigger(true, 1500); const [facets, setFacets] = useState<{ - results: any[]; + results: ListingResult[]; total: number; aggregations: Aggregations; } | null>(null); @@ -241,11 +292,10 @@ const ListingComponent: React.FC = ({ const count = facets?.total ?? 0; const datasetDetails = facets?.results ?? []; - // Stabilize lockedFilters reference to prevent infinite loops + const lockedFiltersKey = JSON.stringify(lockedFilters); const stableLockedFilters = useMemo( - () => lockedFilters, - // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(lockedFilters)] + () => JSON.parse(lockedFiltersKey) as typeof lockedFilters, + [lockedFiltersKey] ); useUrlParams(queryParams, setQueryParams, setVariables, stableLockedFilters); @@ -268,11 +318,7 @@ const ListingComponent: React.FC = ({ } }, [variables, type]); - const [hasMounted, setHasMounted] = useState(false); - - useEffect(() => { - setHasMounted(true); - }, []); + const hasMounted = useMounted(); if (!hasMounted) { if (type === 'usecase') { return ; @@ -528,7 +574,7 @@ const ListingComponent: React.FC = ({ onPageSizeChange={handlePageSizeChange} view={view} > - {datasetDetails.map((item: any, index: number) => { + {datasetDetails.map((item, index) => { const image = item.is_individual_dataset ? item?.user?.profile_picture ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.user.profile_picture}` @@ -545,9 +591,9 @@ const ListingComponent: React.FC = ({ const sdgs = item.sdgs && item.sdgs.length > 0 ? item.sdgs : null; - const MetadataContent = [ + const MetadataContent: CardMetadataItem[] = [ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', value: formatDate(item.modified) || '', tooltip: 'Date', @@ -555,9 +601,9 @@ const ListingComponent: React.FC = ({ }, ]; - if (item.download_count > 0) { + if ((item.download_count ?? 0) > 0) { MetadataContent.push({ - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: item.download_count || 0, tooltip: 'Download', @@ -570,7 +616,7 @@ const ListingComponent: React.FC = ({ const geoDisplay = geographies.join(', '); MetadataContent.push({ - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: geoDisplay, tooltip: geoDisplay, @@ -581,11 +627,11 @@ const ListingComponent: React.FC = ({ if (sdgs && sdgs.length > 0) { // Format SDGs for display const sdgDisplay = sdgs - .map((sdg: any) => `${sdg.code} - ${sdg.name}`) + .map((sdg) => `${sdg.code} - ${sdg.name}`) .join(', '); MetadataContent.push({ - icon: Icons.target as any, + icon: Icons['target' as keyof typeof Icons], label: 'SDG Goals', value: sdgDisplay, tooltip: sdgDisplay, @@ -595,7 +641,7 @@ const ListingComponent: React.FC = ({ if (item.has_charts && view === 'expanded') { MetadataContent.push({ - icon: Icons.chart as any, + icon: Icons.chart, label: '', value: 'With Charts', tooltip: 'Charts', @@ -603,16 +649,16 @@ const ListingComponent: React.FC = ({ }); } - const LeftFooterChips = [ + const LeftFooterChips: FooterChip[] = [ { - icon: `/Sectors/${item.sectors?.[0]}.svg` as any, + icon: `/Sectors/${item.sectors?.[0]}.svg`, label: 'Sectors', tooltip: `${item.sectors?.[0]}`, }, ...(item.has_charts && view !== 'expanded' ? [ { - icon: `/chart-bar.svg` as any, + icon: `/chart-bar.svg`, label: 'Charts', tooltip: 'Charts', }, @@ -620,9 +666,9 @@ const ListingComponent: React.FC = ({ : []), ]; - const RightFooterChips = [ + const RightFooterChips: FooterChip[] = [ { - icon: image as any, + icon: image, label: 'Published by', tooltip: `${item.is_individual_dataset ? item.user?.name : item.organization?.name}`, }, @@ -631,7 +677,7 @@ const ListingComponent: React.FC = ({ const commonProps = { title: item.title, description: stripMarkdown(item.description || ''), - metadataContent: MetadataContent as any, + metadataContent: MetadataContent as CardMetadataTuple, tag: item.tags, formats: item.formats, leftFooterChips: LeftFooterChips, diff --git a/app/[locale]/(user)/components/Sectors.tsx b/app/[locale]/(user)/components/Sectors.tsx index 73880765..da3e4bcf 100644 --- a/app/[locale]/(user)/components/Sectors.tsx +++ b/app/[locale]/(user)/components/Sectors.tsx @@ -62,11 +62,17 @@ const Sectors = () => { ) : (
- {data?.activeSectors.map((sector: any) => ( + {data?.activeSectors.map((sector) => ( ))} diff --git a/app/[locale]/(user)/components/UseCases.tsx b/app/[locale]/(user)/components/UseCases.tsx index e664724b..ba3b262e 100644 --- a/app/[locale]/(user)/components/UseCases.tsx +++ b/app/[locale]/(user)/components/UseCases.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/navigation'; import { graphql } from '@/gql'; +import { UseCaseStatus } from '@/gql/generated/graphql'; import { useQuery } from '@tanstack/react-query'; import { Button, @@ -21,7 +22,7 @@ import { UseCaseListingSkeleton } from '@/components/loading'; import { stripMarkdown } from '../search/components/UnifiedListingComponent'; import Styles from './datasets.module.scss'; -const useCasesListDoc: any = graphql(` +const useCasesListDoc = graphql(` query TopUseCases( $filters: UseCaseFilter $pagination: OffsetPaginationInput @@ -81,17 +82,12 @@ const useCasesListDoc: any = graphql(` `); const UseCasesListingPage = () => { - const getUseCasesList: { - data: any; - isLoading: boolean; - error: any; - isError: boolean; - } = useQuery([`useCases_list`], () => + const getUseCasesList = useQuery([`useCases_list`], () => GraphQL( useCasesListDoc, {}, { - filters: { status: 'PUBLISHED' }, + filters: { status: UseCaseStatus.Published }, pagination: { limit: 6 }, } ) @@ -134,9 +130,9 @@ const UseCasesListingPage = () => { ) : ( {getUseCasesList && - getUseCasesList?.data?.publishedUseCases.length > 0 && - getUseCasesList?.data?.publishedUseCases.map( - (item: any, index: any) => ( + (getUseCasesList.data?.publishedUseCases?.length ?? 0) > 0 && + getUseCasesList.data?.publishedUseCases.map( + (item, index) => ( { )} > 0 + item.geographies && item.geographies.length > 0 ? item.geographies - .map((geo: any) => geo.name) + .map((geo) => geo.name) .join(', ') : '', stroke: 1.2, @@ -169,24 +165,24 @@ const UseCasesListingPage = () => { ]} leftFooterChips={[ { - icon: `/Sectors/${item?.sectors[0]?.name}.svg` as any, + icon: `/Sectors/${item?.sectors?.[0]?.name}.svg`, label: 'Sectors', }, ]} rightFooterChips={[ { icon: item.isIndividualUsecase - ? (item?.user?.profilePicture as any) + ? item?.user?.profilePicture ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.user.profilePicture.url}` : '/profile.png' : item?.organization?.logo ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.organization.logo.url}` - : ('/org.png' as any), + : '/org.png', label: 'Published by', }, ]} imageUrl={`${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.logo?.path.replace('/code/files/', '')}`} - description={stripMarkdown(item.summary)} + description={stripMarkdown(item.summary || '')} iconColor="metadata" variation={'collapsed'} // type={[ diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx index 273088d3..167bfb48 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/DatasetDetailsPage.tsx @@ -15,7 +15,7 @@ import Resources from './components/Resources'; import SimilarDatasets from './components/SimilarDatasets'; import { useTourTrigger } from '@/hooks/use-tour-trigger'; -const datasetQuery: any = graphql(` +const datasetQuery = graphql(` query getDataset($datasetId: UUID!) { getDataset(datasetId: $datasetId) { tags { @@ -88,7 +88,7 @@ export default function DatasetDetailsPage({ // Enable tour for first-time users useTourTrigger(true, 1500); - const Datasetdetails: { data: any; isLoading: any } = useQuery( + const Datasetdetails = useQuery( [`details_${datasetId}`], () => GraphQL(datasetQuery, {}, { datasetId: datasetId }) ); diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/AccessModels/index.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/AccessModels/index.tsx index 40d1d8ab..ad3bd49b 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/AccessModels/index.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/AccessModels/index.tsx @@ -1,6 +1,7 @@ import Link from 'next/link'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; +import { AccessModelResourceQuery } from '@/gql/generated/graphql'; import { useQuery } from '@tanstack/react-query'; import { Accordion, @@ -19,6 +20,31 @@ import { GraphQL } from '@/lib/api'; import CustomTags from '@/components/CustomTags'; import { Icons } from '@/components/icons'; +interface SchemaField { + fieldName: string; + format: string; +} + +interface AccessModelTableRow { + original: { + schema: SchemaField[]; + download: string; + }; +} + +interface ModelResource { + resource: { + name: string; + description?: string | null; + id: string; + }; + fields: SchemaField[]; +} + +type AccessModelItem = AccessModelResourceQuery['accessModelResources'][number] & { + modelResources?: ModelResource[]; +}; + const generateColumnData = () => { return [ { @@ -32,7 +58,7 @@ const generateColumnData = () => { { accessorKey: 'schema', header: 'Fields', - cell: ({ row }: any) => { + cell: ({ row }: { row: AccessModelTableRow }) => { return ( @@ -55,7 +81,7 @@ const generateColumnData = () => { header: 'Format', }, ]} - rows={row.original.schema.map((field: any) => ({ + rows={row.original.schema.map((field) => ({ name: field.fieldName, format: field.format, }))} @@ -69,7 +95,7 @@ const generateColumnData = () => { { accessorKey: 'download', header: 'Download', - cell: ({ row }: any) => { + cell: ({ row }: { row: AccessModelTableRow }) => { return ( { ]; }; -const generateTableData = (resources: any[]) => { - return resources.map((item: any) => ({ +const generateTableData = (resources: ModelResource[]) => { + return resources.map((item) => ({ resourceName: item.resource.name, description: item.resource.description, download: item.resource.id, @@ -93,7 +119,7 @@ const generateTableData = (resources: any[]) => { })); }; -const accessModelResourcesQuery: any = graphql(` +const accessModelResourcesQuery = graphql(` query accessModelResource($datasetId: UUID!) { accessModelResources(datasetId: $datasetId) { resourceFields { @@ -117,20 +143,18 @@ const accessModelResourcesQuery: any = graphql(` const AccessModels = () => { const params = useParams(); - const getAccessModeldetails: { - data: any; - isError: boolean; - isLoading: boolean; - } = useQuery([`accessmodel_${params.datasetIdentifier}`], () => - GraphQL( - accessModelResourcesQuery, - { - // Entity Headers if present - }, - { - datasetId: params.datasetIdentifier, - } - ) + const getAccessModeldetails = useQuery( + [`accessmodel_${params.datasetIdentifier}`], + () => + GraphQL( + accessModelResourcesQuery, + { + // Entity Headers if present + }, + { + datasetId: params.datasetIdentifier, + } + ) ); return ( @@ -141,7 +165,7 @@ const AccessModels = () => {
) : ( getAccessModeldetails.data?.accessModelResources.map( - (item: any, index: any) => ( + (item: AccessModelItem, index: number) => (
{
- +
- {item?.modelResources?.length > 0 && ( + {(item.modelResources?.length ?? 0) > 0 && (
@@ -171,7 +197,7 @@ const AccessModels = () => { className="h-fit w-fit" kind="secondary" onClick={() => { - item.modelResources.forEach((resource: any) => { + item.modelResources?.forEach((resource) => { // Construct the download URL for each resource const downloadUrl = `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/download/resource/${resource.resource.id}`; // Open the URL in a new tab @@ -191,7 +217,7 @@ const AccessModels = () => { > diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Details/index.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Details/index.tsx index eb4430d1..3b42a5f7 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Details/index.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Details/index.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { useParams } from 'next/navigation'; import { renderGeoJSON } from '@/geo_json/render_geojson'; import { graphql } from '@/gql'; +import { ChartDetailsQueryQuery } from '@/gql/generated/graphql'; import { useQuery } from '@tanstack/react-query'; import ReactECharts from 'echarts-for-react'; import * as echarts from 'echarts/core'; @@ -23,7 +24,7 @@ import { GraphQL } from '@/lib/api'; import { useAnalytics } from '@/hooks/use-analytics'; import { Icons } from '@/components/icons'; -const DetailsQuery: any = graphql(` +const DetailsQuery = graphql(` query ChartDetailsQuery($datasetId: UUID!) { getChartData(datasetId: $datasetId) { __typename @@ -55,7 +56,7 @@ const Details: React.FC = () => { const chartRef = useRef(null); const { trackDataset } = useAnalytics(); - const { data, isLoading }: { data: any; isLoading: any } = useQuery( + const { data, isLoading } = useQuery( [`chartDetails_${params.id}`], () => GraphQL(DetailsQuery, {}, { datasetId: params.datasetIdentifier }) ); @@ -67,13 +68,18 @@ const Details: React.FC = () => { } }, [params.datasetIdentifier, trackDataset]); - const renderChart = (item: any) => { + type ChartItem = ChartDetailsQueryQuery['getChartData'][number]; + type ResourceChart = Extract; + + const renderChart = (item: ResourceChart) => { if (item.chartType === 'ASSAM_DISTRICT' || item.chartType === 'ASSAM_RC') { - // Register the map - echarts.registerMap( - item.chartType.toLowerCase(), - renderGeoJSON(item.chartType.toLowerCase()) - ); + const geoJson = renderGeoJSON(item.chartType.toLowerCase()); + if (geoJson && geoJson.type === 'FeatureCollection') { + echarts.registerMap( + item.chartType.toLowerCase(), + geoJson as Parameters[1] + ); + } } return ; @@ -88,23 +94,24 @@ const Details: React.FC = () => {
- ) : data?.getChartData?.length > 0 ? ( + ) : (data?.getChartData?.length ?? 0) > 0 ? (
- {data?.getChartData.map((item: any, index: any) => ( + {data?.getChartData.map((item, index) => (
{item.name} - {item.description.length > 260 && !isexpanded - ? `${item.description.slice(0, 260)}...` + {(item.description ?? '').length > 260 && + !isexpanded + ? `${(item.description ?? '').slice(0, 260)}...` : item.description} - {item.description.length > 260 && ( + {(item.description ?? '').length > 260 && (
diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/PrimaryData/index.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/PrimaryData/index.tsx index 63e5dfc2..eff9e4e1 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/PrimaryData/index.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/PrimaryData/index.tsx @@ -3,11 +3,12 @@ import React, { useState } from 'react'; import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui'; +import { GetDatasetQuery } from '@/gql/generated/graphql'; import { Icons } from '@/components/icons'; import Metadata from '../Metadata'; interface PrimaryDataProps { - data: any; + data: GetDatasetQuery['getDataset']; isLoading?: boolean; } @@ -19,7 +20,7 @@ const PrimaryData: React.FC = ({ data, isLoading }) => {
{data?.title}
- {data?.tags.map((item: any, index: any) => ( + {data?.tags.map((item, index) => ( unknown; +} + const Resources = () => { const params = useParams(); - const getResourceDetails: { data: any; isLoading: boolean } = useQuery( + const getResourceDetails = useQuery( [`resources_${params.datasetIdentifier}`], () => GraphQL( @@ -60,7 +86,7 @@ const Resources = () => { { accessorKey: 'schema', header: 'Columns', - cell: ({ row }: any) => { + cell: ({ row }: { row: ResourceTableRow }) => { return ( @@ -91,7 +117,7 @@ const Resources = () => { header: 'Format', }, ]} - rows={row.original.schema.map((item: any) => ({ + rows={row.original.schema.map((item) => ({ name: item.fieldName, format: item.format, description: item.description, @@ -105,7 +131,7 @@ const Resources = () => { { accessorKey: 'rowsLength', header: 'No.of Rows', - cell: ({ row }: any) => { + cell: ({ row }: { row: ResourceTableRow }) => { return (

{row.original.rowsLength === 0 @@ -126,7 +152,7 @@ const Resources = () => { { accessorKey: 'preview', header: 'Preview', - cell: ({ row }: any) => { + cell: ({ row }: { row: ResourceTableRow }) => { const previewData = row.original.preview; // Generate columns dynamically from previewData.columns @@ -134,7 +160,7 @@ const Resources = () => { previewData?.columns?.map((column: string) => ({ accessorKey: column, header: column, - cell: ({ cell }: any) => { + cell: ({ cell }: { cell: PreviewCell }) => { const value = cell.getValue(); return ( {value !== null ? value?.toString() : 'N/A'} @@ -144,8 +170,8 @@ const Resources = () => { // Transform rows data to match column structure const previewRows = - previewData?.rows?.map((row: any[]) => { - const rowData: Record = {}; + previewData?.rows?.map((row) => { + const rowData: Record = {}; previewData.columns.forEach((column: string, index: number) => { rowData[column] = row[index]; }); @@ -190,13 +216,16 @@ const Resources = () => { ]; }; - const generateTableData = (data: any) => { + const generateTableData = ( + data: DatasetResourcesQuery['datasetResources'][number] + ) => { return [ { schema: data?.schema, rowsLength: data?.noOfEntries || 'Na', format: data?.fileDetails?.format || 'Na', - size: Math.round(data?.fileDetails?.size / 1024).toFixed(2) + 'KB', + size: + Math.round((data?.fileDetails?.size ?? 0) / 1024).toFixed(2) + 'KB', preview: data?.previewData, id: data?.id, }, @@ -220,7 +249,7 @@ const Resources = () => {

{getResourceDetails.data?.datasetResources.map( - (item: any, index: number) => ( + (item, index: number) => (
{ const params = useParams(); - const SimilatDatasetdetails: { data: any; isLoading: any } = useQuery( + const SimilatDatasetdetails = useQuery( [`similar_datasets_${params.datasetIdentifier}`], () => GraphQL( @@ -112,40 +112,42 @@ const SimilarDatasets: React.FC = () => { {SimilatDatasetdetails?.data?.getDataset && SimilatDatasetdetails?.data?.getDataset.similarDatasets.map( - (item: any) => { + (item) => { const geographies = Array.isArray(item.geographies) && item.geographies.length > 0 ? item.geographies - .map((geo: any) => + .map((geo) => typeof geo === 'string' ? geo : geo?.name ) .filter(Boolean) : null; - const metadataContent: any[] = [ - { - icon: Icons.calendarEvent as any, - label: 'Date', - value: '19 July 2024', - stroke: 1.2, - }, - { - icon: Icons.fileDownload as any, - label: 'Download', - value: item.downloadCount.toString(), - stroke: 1.2, - }, - ]; - - if (geographies && geographies.length > 0) { - metadataContent.push({ - icon: Icons.worldPin as any, - label: 'Geography', - value: geographies.join(', '), - stroke: 1.2, - }); - } + const dateMeta = { + icon: Icons.calendarEvent, + label: 'Date', + value: '19 July 2024', + stroke: 1.2, + }; + const downloadMeta = { + icon: Icons.fileDownload, + label: 'Download', + value: item.downloadCount.toString(), + stroke: 1.2, + }; + const metadataContent = + geographies && geographies.length > 0 + ? ([ + dateMeta, + downloadMeta, + { + icon: Icons.worldPin, + label: 'Geography', + value: geographies.join(', '), + stroke: 1.2, + }, + ] as const) + : ([dateMeta, downloadMeta] as const); return ( { t.value)} formats={item.formats} leftFooterChips={[ { - icon: `/Sectors/${item.sectors[0]?.name}.svg` as any, + icon: `/Sectors/${item.sectors[0]?.name}.svg`, label: 'Sectors', }, ]} rightFooterChips={[ { icon: item.isIndividualDataset - ? (item?.user?.profilePicture as any) + ? item?.user?.profilePicture ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.user.profilePicture.url}` - : ('/profile.png' as any) - : (item?.organization?.logo as any) + : '/profile.png' + : item?.organization?.logo ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${item.organization.logo.url}` - : ('/org.png' as any), + : '/org.png', label: 'Published by', }, ]} diff --git a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Visualizations/index.tsx b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Visualizations/index.tsx index 2ec5da60..18b1697f 100644 --- a/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Visualizations/index.tsx +++ b/app/[locale]/(user)/datasets/[datasetIdentifier]/components/Visualizations/index.tsx @@ -2,8 +2,13 @@ import React from 'react'; import { Button, Text } from 'opub-ui'; import { BarChart } from 'opub-ui/viz'; +interface VisualizationItem { + title: string; + description: string; +} + interface VisualizationProps { - data: any; + data: VisualizationItem[]; } const Visualization: React.FC = ({ data }) => { @@ -77,7 +82,7 @@ const Visualization: React.FC = ({ data }) => { }; return (
- {data.map((item: any, index: any) => ( + {data.map((item, index) => (
tag.value) || [], + title: `${dataset?.title ?? ''} | Dataset | CivicDataSpace`, + description: dataset?.description ?? undefined, + keywords: dataset?.tags?.map((tag) => tag.value) || [], openGraph: { type: 'dataset', locale: 'en_US', url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/datasets/${datasetIdentifier}`, - title: dataset?.title, - description: dataset?.description, + title: dataset?.title ?? '', + description: dataset?.description ?? '', siteName: 'CivicDataSpace', image: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/og.png`, }, }); } catch (e) { - console.error('Metadata fetch error', e); + if (!isUnpublishedDatasetError(e)) { + console.error('Metadata fetch error', e); + } return generatePageMetadata({ title: 'Dataset Details' }); } } diff --git a/app/[locale]/(user)/datasets/components/FIlter/GeographyFilter.tsx b/app/[locale]/(user)/datasets/components/FIlter/GeographyFilter.tsx index 57bc52af..07ba7594 100644 --- a/app/[locale]/(user)/datasets/components/FIlter/GeographyFilter.tsx +++ b/app/[locale]/(user)/datasets/components/FIlter/GeographyFilter.tsx @@ -5,7 +5,7 @@ import { AccordionTrigger, Text, } from 'opub-ui'; -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState } from 'react'; import { TreeView } from '@/components/ui/tree-view'; import { toTitleCase } from '@/lib/utils'; @@ -26,6 +26,31 @@ interface GeographyNode extends Geography { children: GeographyNode[]; } +function buildHierarchy(flatList: Geography[]): GeographyNode[] { + const map = new Map(); + const roots: GeographyNode[] = []; + + flatList.forEach((geo) => { + map.set(geo.id, { ...geo, children: [] }); + }); + + flatList.forEach((geo) => { + const node = map.get(geo.id)!; + if (geo.parentId && geo.parentId.id) { + const parent = map.get(geo.parentId.id); + if (parent) { + parent.children.push(node); + } else { + roots.push(node); + } + } else { + roots.push(node); + } + }); + + return roots; +} + interface GeographyFilterProps { selectedGeographies: string[]; onGeographyChange: (geographies: string[]) => void; @@ -40,12 +65,22 @@ const GeographyFilter: React.FC = ({ const [geographies, setGeographies] = useState([]); const [loading, setLoading] = useState(true); const [expandedItems, setExpandedItems] = useState([]); - const geographyOptionsRef = useRef(geographyOptions); - geographyOptionsRef.current = geographyOptions; useEffect(() => { + const fallbackFromOptions = () => { + if (geographyOptions.length === 0) return; + const flatGeographies: GeographyNode[] = geographyOptions.map((opt, idx) => ({ + id: idx, + name: opt.label, + code: '', + type: '', + parentId: null, + children: [], + })); + setGeographies(flatGeographies); + }; + const fetchGeographies = async () => { - setLoading(true); try { // Always try to fetch from GraphQL for full hierarchy const response = await fetch( @@ -88,69 +123,23 @@ const GeographyFilter: React.FC = ({ if (data && data.geographies && data.geographies.length > 0) { const hierarchicalData = buildHierarchy(data.geographies); setGeographies(hierarchicalData); - } else if (geographyOptionsRef.current && geographyOptionsRef.current.length > 0) { - // Fallback to aggregations if GraphQL fails - const flatGeographies: GeographyNode[] = geographyOptionsRef.current.map((opt, idx) => ({ - id: idx, - name: opt.label, - code: '', - type: '', - parentId: null, - children: [], - })); - setGeographies(flatGeographies); + } else { + fallbackFromOptions(); } } catch (error) { console.error('Error fetching geographies:', error); - // Use aggregations as fallback on error - if (geographyOptionsRef.current && geographyOptionsRef.current.length > 0) { - const flatGeographies: GeographyNode[] = geographyOptionsRef.current.map((opt, idx) => ({ - id: idx, - name: opt.label, - code: '', - type: '', - parentId: null, - children: [], - })); - setGeographies(flatGeographies); - } + fallbackFromOptions(); } finally { setLoading(false); } }; - fetchGeographies(); - }, []); - - const buildHierarchy = (flatList: Geography[]): GeographyNode[] => { - const map = new Map(); - const roots: GeographyNode[] = []; - - // Initialize all nodes - flatList.forEach((geo) => { - map.set(geo.id, { ...geo, children: [] }); - }); - - // Build hierarchy - flatList.forEach((geo) => { - const node = map.get(geo.id)!; - if (geo.parentId && geo.parentId.id) { - const parent = map.get(geo.parentId.id); - if (parent) { - parent.children.push(node); - } else { - roots.push(node); - } - } else { - roots.push(node); - } - }); + void fetchGeographies(); + }, [geographyOptions]); - return roots; - }; + type TreeDataItem = React.ComponentProps['data'][number]; - // Convert GeographyNode to TreeView format - const convertToTreeData = (nodes: GeographyNode[]): any[] => { + const convertToTreeData = (nodes: GeographyNode[]): TreeDataItem[] => { return nodes.map((node) => ({ id: node.name, name: node.name, diff --git a/app/[locale]/(user)/datasets/components/ResourceTable/index.tsx b/app/[locale]/(user)/datasets/components/ResourceTable/index.tsx index 669e8f80..29c717a0 100644 --- a/app/[locale]/(user)/datasets/components/ResourceTable/index.tsx +++ b/app/[locale]/(user)/datasets/components/ResourceTable/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import React from 'react'; import { Button, Dialog, Table } from 'opub-ui'; @@ -11,7 +12,7 @@ interface ColumnData { } interface RowData { - [key: string]: any; + [key: string]: unknown; } interface ResourceTableProps { @@ -20,7 +21,9 @@ interface ResourceTableProps { } interface CellProps { - row: RowData; + row: { + original: RowData; + }; } const ResourceTable: React.FC = ({ @@ -32,23 +35,22 @@ const ResourceTable: React.FC = ({ return { ...column, cell: ({ row }: CellProps) => { - const rowData = row.original as unknown as RowData; - const accessorKey = column.accessorKey as keyof RowData; - const cellValue = rowData[accessorKey]; + const rowData = row.original; + const cellValue = rowData[column.accessorKey]; return ( - {column?.table ? ( + {column?.table && Array.isArray(cellValue) ? (
) : ( - cellValue + (cellValue as ReactNode) )} diff --git a/app/[locale]/(user)/layout.tsx b/app/[locale]/(user)/layout.tsx index fb4a462f..8bcf2886 100644 --- a/app/[locale]/(user)/layout.tsx +++ b/app/[locale]/(user)/layout.tsx @@ -1,7 +1,7 @@ 'use client'; import { notFound, usePathname } from 'next/navigation'; -import React, { useEffect, useState } from 'react'; +import React, { useSyncExternalStore } from 'react'; import MainFooter from '../dashboard/components/main-footer'; import { MainNav } from '../dashboard/components/main-nav'; @@ -16,22 +16,13 @@ export default function Layout({ children }: UserLayoutProps) { const user = true; // await getCurrentUser() const routerPath = usePathname(); const hideSearch = routerPath === '/' || routerPath === '/datasets'; - const [isCollaborativeSubdomain, setIsCollaborativeSubdomain] = useState< - boolean | null - >(null); + const isCollaborativeSubdomain = useSyncExternalStore( + () => () => {}, + () => isCollaborativeSubdomainHost(window.location.hostname), + () => null + ); const shouldHideMainNav = isCollaborativeSubdomain === true; - useEffect(() => { - if (typeof window === 'undefined') { - setIsCollaborativeSubdomain(false); - return; - } - - setIsCollaborativeSubdomain( - isCollaborativeSubdomainHost(window.location.hostname) - ); - }, [routerPath]); - if (!user) { return notFound(); } diff --git a/app/[locale]/(user)/publishers/PublisherCard.tsx b/app/[locale]/(user)/publishers/PublisherCard.tsx index 0ea9b437..c151e23d 100644 --- a/app/[locale]/(user)/publishers/PublisherCard.tsx +++ b/app/[locale]/(user)/publishers/PublisherCard.tsx @@ -1,21 +1,22 @@ import React from 'react'; import Image from 'next/image'; import Link from 'next/link'; +import { PublishersListQuery } from '@/gql/generated/graphql'; import { Text, Tooltip } from 'opub-ui'; interface CardProps { - data: any; + data: PublishersListQuery['getPublishers']; } const PublisherCard: React.FC = ({ data }) => { return (
- {data.map((item: any, index: any) => ( + {data.map((item, index) => ( = ({ data }) => {
*/}
- {(item?.bio || item?.description) && ( + {(item.__typename === 'TypeUser' + ? item.bio + : item.description) && (
{item.__typename === 'TypeUser' - ? item?.bio?.length > 220 + ? item.bio && item.bio.length > 220 ? item.bio.slice(0, 220) + '...' : item.bio - : item?.description?.length > 220 + : item.description.length > 220 ? item.description.slice(0, 220) + '...' : item.description} diff --git a/app/[locale]/(user)/publishers/PublishersListingClient.tsx b/app/[locale]/(user)/publishers/PublishersListingClient.tsx index a17a5fab..55d7e030 100644 --- a/app/[locale]/(user)/publishers/PublishersListingClient.tsx +++ b/app/[locale]/(user)/publishers/PublishersListingClient.tsx @@ -13,7 +13,7 @@ import JsonLd from '@/components/JsonLd'; import PublisherCard from './PublisherCard'; import { PublisherListingSkeleton } from '@/components/loading'; -const getAllPublishers: any = graphql(` +const getAllPublishers = graphql(` query PublishersList { getPublishers { __typename @@ -44,13 +44,8 @@ const getAllPublishers: any = graphql(` const PublishersListingPage = () => { const [type, setType] = useState<'all' | 'org' | 'pub'>('all'); - const Details: { - data: any; - isLoading: boolean; - isError: boolean; - refetch: any; - } = useQuery(['publishers_list_page'], () => - GraphQL(getAllPublishers, {}, []) + const Details = useQuery(['publishers_list_page'], () => + GraphQL(getAllPublishers, {}) ); type PublisherType = 'all' | 'org' | 'pub'; @@ -61,7 +56,7 @@ const PublishersListingPage = () => { ]; const filteredPublishers = Details?.data?.getPublishers?.filter( - (publisher: any) => { + (publisher) => { if (type === 'all') return true; if (type === 'pub') return publisher.__typename === 'TypeUser'; if (type === 'org') return publisher.__typename === 'TypeOrganization'; @@ -72,9 +67,10 @@ const PublishersListingPage = () => { const jsonLd = generateJsonLd({ '@context': 'https://schema.org', '@type': 'Dataset', - name: Details?.data?.getPublishers?.title, + name: 'Our Publishers', url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/publishers`, - description: Details?.data?.getPublishers?.description, + description: + 'Meet the data providers powering CivicDataSpace — explore individual and organizational publishers across domains who are opening up data for impact and transparency.', publisher: { '@type': 'Organization', name: 'CivicDataSpace', @@ -176,7 +172,7 @@ const PublishersListingPage = () => { ) : ( Details.data && Details.data.getPublishers.length > 0 && ( - + ) )}
diff --git a/app/[locale]/(user)/publishers/[publisherSlug]/PublisherPageClient.tsx b/app/[locale]/(user)/publishers/[publisherSlug]/PublisherPageClient.tsx index 2ea84bad..607b053a 100644 --- a/app/[locale]/(user)/publishers/[publisherSlug]/PublisherPageClient.tsx +++ b/app/[locale]/(user)/publishers/[publisherSlug]/PublisherPageClient.tsx @@ -12,7 +12,7 @@ import JsonLd from '@/components/JsonLd'; import ProfileDetails from '../components/ProfileDetails'; import SidebarCard from '../components/SidebarCard'; -const userInfoQuery: any = graphql(` +const userInfoQuery = graphql(` query UserData($userId: ID!) { userById(userId: $userId) { id @@ -34,7 +34,7 @@ const userInfoQuery: any = graphql(` `); const PublisherPageClient = ({ publisherSlug }: { publisherSlug: string }) => { - const userInfo: any = useQuery([`${publisherSlug}`], () => + const userInfo = useQuery([`${publisherSlug}`], () => GraphQL( userInfoQuery, { diff --git a/app/[locale]/(user)/publishers/components/Datasets.tsx b/app/[locale]/(user)/publishers/components/Datasets.tsx index b21236e9..058270da 100644 --- a/app/[locale]/(user)/publishers/components/Datasets.tsx +++ b/app/[locale]/(user)/publishers/components/Datasets.tsx @@ -1,5 +1,9 @@ import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; +import { + OrganizationPublishedDatasetsListQuery, + UserPublishedDatasetsListQuery, +} from '@/gql/generated/graphql'; import { useQuery } from '@tanstack/react-query'; import { Card, Icon, Spinner, Text } from 'opub-ui'; @@ -8,7 +12,7 @@ import { cn, extractPublisherId } from '@/lib/utils'; import { Icons } from '@/components/icons'; import { stripMarkdown } from '../../search/components/UnifiedListingComponent'; -const userPublishedDatasetsDoc: any = graphql(` +const userPublishedDatasetsDoc = graphql(` query userPublishedDatasetsList($userId: ID!) { userPublishedDatasets(userId: $userId) { id @@ -55,7 +59,7 @@ const userPublishedDatasetsDoc: any = graphql(` } `); -const organizationPublishedDatasetsDoc: any = graphql(` +const organizationPublishedDatasetsDoc = graphql(` query organizationPublishedDatasetsList($organizationId: ID!) { organizationPublishedDatasets(organizationId: $organizationId) { id @@ -108,9 +112,17 @@ const Datasets = ({ type }: { type: 'organization' | 'Publisher' }) => { String(type === 'organization' ? params.organizationSlug : params.publisherSlug) ); - const PublishedDatasetsList: any = useQuery( + type PublishedDatasetsData = + | OrganizationPublishedDatasetsListQuery + | UserPublishedDatasetsListQuery; + + type PublishedDataset = + | OrganizationPublishedDatasetsListQuery['organizationPublishedDatasets'][number] + | UserPublishedDatasetsListQuery['userPublishedDatasets'][number]; + + const PublishedDatasetsList = useQuery( ['publishedDatasets', type, id], - () => + (): Promise => type === 'organization' ? GraphQL( organizationPublishedDatasetsDoc, @@ -128,10 +140,11 @@ const Datasets = ({ type }: { type: 'organization' | 'Publisher' }) => { ) ); - const DatasetData = - type === 'organization' - ? PublishedDatasetsList.data?.organizationPublishedDatasets - : PublishedDatasetsList.data?.userPublishedDatasets; + const DatasetData = PublishedDatasetsList.data + ? 'organizationPublishedDatasets' in PublishedDatasetsList.data + ? PublishedDatasetsList.data.organizationPublishedDatasets + : PublishedDatasetsList.data.userPublishedDatasets + : undefined; return (
@@ -144,8 +157,8 @@ const Datasets = ({ type }: { type: 'organization' | 'Publisher' }) => {
- ) : DatasetData?.length > 0 ? ( - DatasetData?.map((item: any, index: any) => ( + ) : (DatasetData?.length ?? 0) > 0 ? ( + DatasetData?.map((item: PublishedDataset, index: number) => ( { description={stripMarkdown(item.description || '')} metadataContent={[ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', value: '19 July 2024', stroke: 1.2, }, { - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: item.downloadCount.toString(), stroke: 1.2, }, { - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: 'India', stroke: 1.2, }, ]} - tag={item.tags} + tag={item.tags.map((t) => t.value)} formats={item.formats} leftFooterChips={[ { diff --git a/app/[locale]/(user)/publishers/components/ProfileDetails.tsx b/app/[locale]/(user)/publishers/components/ProfileDetails.tsx index a52c38a0..9185c74d 100644 --- a/app/[locale]/(user)/publishers/components/ProfileDetails.tsx +++ b/app/[locale]/(user)/publishers/components/ProfileDetails.tsx @@ -7,8 +7,18 @@ import { Icons } from '@/components/icons'; import Datasets from './Datasets'; import UseCases from './UseCases'; +interface ProfileDetailsData { + twitterProfile?: string | null; + linkedinProfile?: string | null; + githubProfile?: string | null; + created?: string | number | null; + dateJoined?: string | number | null; + description?: string | null; + bio?: string | null; +} + interface ProfileDetailsProps { - data: any; + data: ProfileDetailsData | null | undefined; type: 'organization' | 'Publisher'; } @@ -44,8 +54,8 @@ const ProfileDetails: React.FC = ({ data, type }) => { Joined on:{' '} {type === 'organization' - ? formatDate(data?.created) || '' - : formatDate(data?.dateJoined) || ''} + ? formatDate(data?.created ?? null) || '' + : formatDate(data?.dateJoined ?? null) || ''}
@@ -54,10 +64,10 @@ const ProfileDetails: React.FC = ({ data, type }) => {
- {socialMedia?.map((item: any, index: any) => ( + {socialMedia?.map((item, index) => ( diff --git a/app/[locale]/(user)/publishers/components/SidebarCard.tsx b/app/[locale]/(user)/publishers/components/SidebarCard.tsx index db84c10b..2fe9462c 100644 --- a/app/[locale]/(user)/publishers/components/SidebarCard.tsx +++ b/app/[locale]/(user)/publishers/components/SidebarCard.tsx @@ -7,11 +7,25 @@ import { Icon, Text } from 'opub-ui'; import { GraphQL } from '@/lib/api'; import { Icons } from '@/components/icons'; +interface SidebarCardData { + id?: string; + fullName?: string; + name?: string; + logo?: { url: string } | null; + profilePicture?: { url: string } | null; + publishedUseCasesCount?: number; + publishedDatasetsCount?: number; + contributedSectorsCount?: number; + location?: string | null; + linkedinProfile?: string | null; + githubProfile?: string | null; +} + interface SidebarCardProps { - data: any; + data: SidebarCardData | null | undefined; type: 'organization' | 'Publisher'; } -const sectorsDoc: any = graphql(` +const sectorsDoc = graphql(` query sectorInfo($userId: ID!) { userContributedSectors(userId: $userId) { id @@ -21,7 +35,7 @@ const sectorsDoc: any = graphql(` } `); -const organizationDoc: any = graphql(` +const organizationDoc = graphql(` query organizationInfo($organizationId: ID!) { organizationContributedSectors(organizationId: $organizationId) { id @@ -32,30 +46,30 @@ const organizationDoc: any = graphql(` `); const SidebarCard: React.FC = ({ data, type }) => { - const sectorInfo: any = useQuery( - [`${data.id}_sector`], + const sectorInfo = useQuery( + [`${data?.id}_sector`], () => GraphQL( sectorsDoc, { // Entity Headers if present }, - { userId: data.id } + { userId: data?.id ?? '' } ), { enabled: type === 'Publisher' && !!data?.id, } ); - const organizationInfo: any = useQuery( - [`${data.id}_organization`], + const organizationInfo = useQuery( + [`${data?.id}_organization`], () => GraphQL( organizationDoc, { // Entity Headers if present }, - { organizationId: data.id } + { organizationId: data?.id ?? '' } ), { enabled: type === 'organization' && !!data?.id, // runs only if type is 'organization' and data.id exists @@ -68,7 +82,7 @@ const SidebarCard: React.FC = ({ data, type }) => { {type === 'organization' ? ( = ({ data, type }) => { ) : ( = ({ data, type }) => { {type === 'Publisher' && sectorInfo?.data && sectorInfo?.data.userContributedSectors.map( - (item: any, index: any) => ( + (item, index) => (
= ({ data, type }) => { {type === 'organization' && organizationInfo?.data && organizationInfo?.data.organizationContributedSectors.map( - (item: any, index: any) => ( + (item, index) => (
{ String(type === 'organization' ? params.organizationSlug : params.publisherSlug) ); - const PublishedUseCasesList: any = useQuery( + type PublishedUseCasesData = + | OrgPublishedUseCasesListQuery + | UserPublishedUseCasesListQuery; + + type PublishedUseCase = + | OrgPublishedUseCasesListQuery['organizationPublishedUseCases'][number] + | UserPublishedUseCasesListQuery['userPublishedUseCases'][number]; + + interface UseCaseMetadataItem { + metadataItem?: { label?: string | null }; + value?: unknown; + } + + const PublishedUseCasesList = useQuery( ['publishedUseCases', type, id], - () => + (): Promise => type === 'organization' ? GraphQL( orgPublishedUseCasesDoc, @@ -125,10 +142,11 @@ const UseCases = ({ type }: { type: 'organization' | 'Publisher' }) => { ) ); - const UseCaseData = - type === 'organization' - ? PublishedUseCasesList.data?.organizationPublishedUseCases - : PublishedUseCasesList.data?.userPublishedUseCases; + const UseCaseData = PublishedUseCasesList.data + ? 'organizationPublishedUseCases' in PublishedUseCasesList.data + ? PublishedUseCasesList.data.organizationPublishedUseCases + : PublishedUseCasesList.data.userPublishedUseCases + : undefined; return (
@@ -141,8 +159,8 @@ const UseCases = ({ type }: { type: 'organization' | 'Publisher' }) => {
- ) : UseCaseData?.length > 0 ? ( - UseCaseData?.map((item: any, index: any) => ( + ) : (UseCaseData?.length ?? 0) > 0 ? ( + UseCaseData?.map((item: PublishedUseCase, index: number) => ( { // borderColor: '#F9C74F', // }, // ]} - title={item.title} + title={item.title ?? ''} key={index} href={`/usecases/${item.id}`} metadataContent={[ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', value: formatDate(item.modified) || '', stroke: 1.2, }, { - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', - value: item.metadata?.find( - (meta: any) => meta.metadataItem?.label === 'Geography' - )?.value, + value: String( + item.metadata?.find( + (meta: UseCaseMetadataItem) => + meta.metadataItem?.label === 'Geography' + )?.value ?? '' + ), stroke: 1.2, }, ]} leftFooterChips={[ { - icon: `/Sectors/${item?.sectors[0]?.name}.svg` as any, + icon: `/Sectors/${item?.sectors?.[0]?.name}.svg`, label: 'Sectors', }, ]} diff --git a/app/[locale]/(user)/search/components/UnifiedListingComponent.tsx b/app/[locale]/(user)/search/components/UnifiedListingComponent.tsx index 740a00f0..7da56c53 100644 --- a/app/[locale]/(user)/search/components/UnifiedListingComponent.tsx +++ b/app/[locale]/(user)/search/components/UnifiedListingComponent.tsx @@ -1,9 +1,11 @@ 'use client'; import React, { useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import GraphqlPagination from '@/app/[locale]/dashboard/components/GraphqlPagination/graphqlPagination'; +import { useMounted } from '@/hooks/use-mounted'; import { Button, Card, @@ -26,7 +28,7 @@ import Styles from '../../datasets/dataset.module.scss'; export const stripMarkdown = (markdown: string): string => { if (!markdown) return ''; - let cleaned = markdown + const cleaned = markdown // Remove code blocks first (before other replacements) .replace(/```[\s\S]*?```/g, '') // Remove inline code @@ -86,13 +88,89 @@ interface Bucket { interface Aggregation { buckets?: Bucket[]; - [key: string]: any; + [key: string]: unknown; } interface Aggregations { [key: string]: Aggregation; } +interface SearchPublisher { + name?: string; + logo?: string; + profile_picture?: string; +} + +interface SearchSdg { + code: string; + name: string; +} + +interface SearchSector { + name?: string; +} + +interface SearchResult { + type?: string; + id: string | number; + slug?: string; + publisher_type?: string; + is_individual_dataset?: boolean; + is_individual_usecase?: boolean; + is_individual_model?: boolean; + is_individual_collaborative?: boolean; + logo?: string; + profile_picture?: string; + user?: SearchPublisher; + organization?: SearchPublisher; + geographies?: string[]; + sdgs?: SearchSdg[]; + created?: string; + published_datasets_count?: number; + published_usecases_count?: number; + members_count?: number; + started_on?: string; + dataset_count?: number; + modified?: string; + updated_at?: string; + download_count?: number; + has_charts?: boolean; + sectors?: Array; + title?: string; + name?: string; + description?: string; + bio?: string; + tags?: string[]; + formats?: string[]; +} + +interface UnifiedSearchResponse { + results: SearchResult[]; + total: number; + aggregations: Aggregations; + types_searched: string[]; +} + +interface CardMetadataItem { + icon: (typeof Icons)[keyof typeof Icons]; + stroke?: number; + label: string; + value: string | number; + tooltip?: string; +} + +type CardMetadataTuple = + | [] + | [CardMetadataItem] + | [CardMetadataItem, CardMetadataItem] + | [CardMetadataItem, CardMetadataItem, CardMetadataItem]; + +interface FooterChip { + icon: string; + label: string; + tooltip?: string; +} + interface FilterOptions { [key: string]: string[]; } @@ -280,7 +358,7 @@ const fetchUnifiedData = async (variables: string) => { const text = await response.text(); try { - return JSON.parse(text); + return JSON.parse(text) as UnifiedSearchResponse; } catch (e) { console.error( 'JSON Parse Error. Response text:', @@ -304,12 +382,7 @@ const UnifiedListingComponent: React.FC = ({ placeholder, redirectionURL, }) => { - const [facets, setFacets] = useState<{ - results: any[]; - total: number; - aggregations: Aggregations; - types_searched: string[]; - } | null>(null); + const [facets, setFacets] = useState(null); const [variables, setVariables] = useState(''); const [open, setOpen] = useState(false); const [queryParams, setQueryParams] = useReducer(queryReducer, initialState); @@ -330,8 +403,6 @@ const UnifiedListingComponent: React.FC = ({ useEffect(() => { if (variables) { const currentFetchId = ++latestFetchId.current; - setIsLoading(true); - setError(null); fetchUnifiedData(variables) .then((res) => { @@ -350,11 +421,7 @@ const UnifiedListingComponent: React.FC = ({ } }, [variables]); - const [hasMounted, setHasMounted] = useState(false); - - useEffect(() => { - setHasMounted(true); - }, []); + const hasMounted = useMounted(); const handlePageChange = (newPage: number) => { setQueryParams({ type: 'SET_CURRENT_PAGE', payload: newPage }); @@ -453,7 +520,9 @@ const UnifiedListingComponent: React.FC = ({ ); const displayTypeCounts = { ...persistedTypeCounts, ...liveTypeCounts }; - useEffect(() => { + const [prevTypeCounts, setPrevTypeCounts] = useState(typeCounts); + if (typeCounts !== prevTypeCounts) { + setPrevTypeCounts(typeCounts); const counts = Object.entries(typeCounts).reduce( (acc, [key, value]) => { if (typeof value === 'number') { @@ -464,22 +533,22 @@ const UnifiedListingComponent: React.FC = ({ {} as Record ); - if (Object.keys(counts).length === 0) return; + if (Object.keys(counts).length > 0) { + setPersistedTypeCounts((prev) => { + const next = { ...prev }; + let changed = false; - setPersistedTypeCounts((prev) => { - const next = { ...prev }; - let changed = false; + Object.entries(counts).forEach(([key, value]) => { + if (next[key] !== value) { + next[key] = value; + changed = true; + } + }); - Object.entries(counts).forEach(([key, value]) => { - if (next[key] !== value) { - next[key] = value; - changed = true; - } + return changed ? next : prev; }); - - return changed ? next : prev; - }); - }, [typeCounts]); + } + } const getTypeButtonClass = (type: string) => { return `font-normal rounded-full border-1 border-solid border-[#C9cccf] ${queryParams.types === type ? 'font-semibold bg-[#E5EFFD] text-primaryBlue border-[#E5EFFD]' : 'bg-[#F2F7FE]'} hover:bg-[#EDF4FE]`; @@ -488,7 +557,7 @@ const UnifiedListingComponent: React.FC = ({ if (!hasMounted) return ; // Helper function to get redirect URL based on type - const getRedirectUrl = (item: any) => { + const getRedirectUrl = (item: SearchResult) => { switch (item.type) { case 'dataset': return `/datasets/${item.id}`; @@ -518,7 +587,7 @@ const UnifiedListingComponent: React.FC = ({ const singleSelectedType = !isSectionedLanding && selectedTypes.length === 1 ? selectedTypes[0] : null; const tabResults = singleSelectedType - ? results.filter((item: any) => item.type === singleSelectedType) + ? results.filter((item) => item.type === singleSelectedType) : results; const tabTotalCount = singleSelectedType && @@ -526,7 +595,7 @@ const UnifiedListingComponent: React.FC = ({ ? displayTypeCounts[singleSelectedType] : count; - const renderResultCard = (item: any) => { + const renderResultCard = (item: SearchResult) => { const isIndividual = item.is_individual_dataset || item.is_individual_usecase || @@ -553,31 +622,31 @@ const UnifiedListingComponent: React.FC = ({ item.geographies && item.geographies.length > 0 ? item.geographies : null; const sdgs = item.sdgs && item.sdgs.length > 0 ? item.sdgs : null; - const MetadataContent = []; + const MetadataContent: CardMetadataItem[] = []; if (item.type === 'publisher') { MetadataContent.push({ - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Joined', - value: formatDate(item.created) || '', + value: formatDate(item.created ?? null) || '', tooltip: 'Date joined', stroke: 1.2, }); MetadataContent.push({ - icon: Icons.dataset as any, + icon: Icons.dataset, label: 'Datasets', value: item.published_datasets_count?.toString() || '0', tooltip: 'Published datasets', }); MetadataContent.push({ - icon: Icons.usecase as any, + icon: Icons['usecase' as keyof typeof Icons], label: 'Use Cases', value: item.published_usecases_count?.toString() || '0', tooltip: 'Published use cases', }); - if (item.publisher_type === 'organization' && item.members_count > 0) { + if (item.publisher_type === 'organization' && (item.members_count ?? 0) > 0) { MetadataContent.push({ - icon: Icons.users as any, + icon: Icons['users' as keyof typeof Icons], label: 'Members', value: item.members_count?.toString() || '0', tooltip: 'Organization members', @@ -585,27 +654,27 @@ const UnifiedListingComponent: React.FC = ({ } } else if (item.type === 'collaborative') { MetadataContent.push({ - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Started', - value: formatDate(item.started_on || item.created) || '', + value: formatDate(item.started_on || item.created || null) || '', stroke: 1.2, }); MetadataContent.push({ - icon: Icons.dataset as any, + icon: Icons.dataset, label: 'Datasets', value: item.dataset_count?.toString() || '0', }); if (geographies && geographies.length > 0) { const geoDisplay = geographies.join(', '); MetadataContent.push({ - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: geoDisplay, stroke: 1.2, }); } else { MetadataContent.push({ - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: 'N/A', stroke: 1.2, @@ -613,9 +682,9 @@ const UnifiedListingComponent: React.FC = ({ } } else { MetadataContent.push({ - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', - value: formatDate(item.modified || item.updated_at) || '', + value: formatDate(item.modified || item.updated_at || null) || '', tooltip: 'Date', stroke: 1.2, }); @@ -623,7 +692,7 @@ const UnifiedListingComponent: React.FC = ({ if (geographies && geographies.length > 0) { const geoDisplay = geographies.join(', '); MetadataContent.push({ - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: geoDisplay, tooltip: geoDisplay, @@ -632,9 +701,9 @@ const UnifiedListingComponent: React.FC = ({ } } - if (item.type === 'dataset' && item.download_count > 0) { + if (item.type === 'dataset' && (item.download_count ?? 0) > 0) { MetadataContent.push({ - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: item.download_count || 0, tooltip: 'Download', @@ -644,10 +713,10 @@ const UnifiedListingComponent: React.FC = ({ if (item.type === 'dataset' && sdgs && sdgs.length > 0) { const sdgDisplay = sdgs - .map((sdg: any) => `${sdg.code} - ${sdg.name}`) + .map((sdg) => `${sdg.code} - ${sdg.name}`) .join(', '); MetadataContent.push({ - icon: Icons.star as any, + icon: Icons.star, label: 'SDG Goals', value: sdgDisplay, tooltip: sdgDisplay, @@ -663,8 +732,8 @@ const UnifiedListingComponent: React.FC = ({ }); } - const LeftFooterChips = []; - const RightFooterChips = []; + const LeftFooterChips: FooterChip[] = []; + const RightFooterChips: FooterChip[] = []; if (item.type === 'publisher') { LeftFooterChips.push({ @@ -699,7 +768,7 @@ const UnifiedListingComponent: React.FC = ({ } } else if (item.sectors && item.sectors.length > 0) { LeftFooterChips.push({ - icon: `/Sectors/${item.sectors?.[0]}.svg` as any, + icon: `/Sectors/${item.sectors?.[0]}.svg`, label: 'Sectors', tooltip: `${item.sectors?.[0]}`, }); @@ -707,7 +776,7 @@ const UnifiedListingComponent: React.FC = ({ if (item.type === 'dataset' && item.has_charts && view !== 'expanded') { LeftFooterChips.push({ - icon: `/chart-bar.svg` as any, + icon: `/chart-bar.svg`, label: 'Charts', tooltip: 'Charts', }); @@ -715,7 +784,7 @@ const UnifiedListingComponent: React.FC = ({ if (item.type !== 'publisher') { RightFooterChips.push({ - icon: image as any, + icon: image, label: 'Published by', tooltip: `${isIndividual ? item.user?.name : item.organization?.name}`, }); @@ -727,7 +796,7 @@ const UnifiedListingComponent: React.FC = ({ // ...((item.type === 'usecase' || item.type === 'dataset') && { // description: stripMarkdown(item.description || item.bio || ''), // }), - metadataContent: MetadataContent as any, + metadataContent: MetadataContent as CardMetadataTuple, tag: item.tags || [], formats: item.type === 'dataset' ? item.formats || [] : [], leftFooterChips: LeftFooterChips, @@ -771,7 +840,7 @@ const UnifiedListingComponent: React.FC = ({ className="flex flex-col gap-4 rounded-4 p-6 shadow-card" >
- = ({ {(item.bio || item.description) && (
- {(item.bio || item.description)?.length > 220 - ? (item.bio || item.description).slice(0, 220) + '...' + {(item.bio || item.description || '').length > 220 + ? (item.bio || item.description || '').slice(0, 220) + '...' : item.bio || item.description}
@@ -1106,8 +1175,8 @@ const UnifiedListingComponent: React.FC = ({ const sectionResults = Array.from( new Map( results - .filter((item: any) => item.type === section.key) - .map((item: any) => [ + .filter((item) => item.type === section.key) + .map((item) => [ `${item.type}-${item.publisher_type || ''}-${item.id}`, item, ]) @@ -1143,7 +1212,7 @@ const UnifiedListingComponent: React.FC = ({
- {sectionResults.map((item: any) => + {sectionResults.map((item) => renderResultCard(item) )}
@@ -1160,7 +1229,7 @@ const UnifiedListingComponent: React.FC = ({ onPageSizeChange={handlePageSizeChange} view={view} > - {tabResults.map((item: any) => renderResultCard(item))} + {tabResults.map((item) => renderResultCard(item))} ) ) : ( diff --git a/app/[locale]/(user)/sectors/SectorsListing.tsx b/app/[locale]/(user)/sectors/SectorsListing.tsx index e0890ca5..ca76c7e4 100644 --- a/app/[locale]/(user)/sectors/SectorsListing.tsx +++ b/app/[locale]/(user)/sectors/SectorsListing.tsx @@ -20,7 +20,7 @@ import { SectorListingSkeleton } from '@/components/loading'; import { SectorCard } from '@/components/SectorCard'; import Styles from '../datasets/dataset.module.scss'; -const sectorsListQueryDoc: any = graphql(` +const sectorsListQueryDoc = graphql(` query SectorsLists($order: SectorOrder, $filters: SectorFilter) { activeSectors(order: $order, filters: $filters) { id @@ -43,7 +43,7 @@ const SectorsListing = () => { sectorsListQueryDoc, {}, { filters: searchText ? { search: searchText } : {}, order: sort } - ) as Promise + ) ); useEffect(() => { @@ -176,9 +176,7 @@ const SectorsListing = () => { value: 'datasetCount_desc', }, ]} - onChange={(e: any) => { - handleSortChange(e); - }} + onChange={handleSortChange} />
@@ -189,11 +187,17 @@ const SectorsListing = () => { ) : data && data?.activeSectors?.length > 0 ? ( <>
- {data?.activeSectors.map((sector: any) => ( + {data?.activeSectors.map((sector) => ( diff --git a/app/[locale]/(user)/usecases/[useCaseSlug]/Dashboards.tsx b/app/[locale]/(user)/usecases/[useCaseSlug]/Dashboards.tsx index 056e45bc..e1ec3e69 100644 --- a/app/[locale]/(user)/usecases/[useCaseSlug]/Dashboards.tsx +++ b/app/[locale]/(user)/usecases/[useCaseSlug]/Dashboards.tsx @@ -7,7 +7,7 @@ import { Text } from 'opub-ui'; import { GraphQL } from '@/lib/api'; import { Loading } from '@/components/loading'; -const DashboardsList: any = graphql(` +const DashboardsList = graphql(` query usecaseDashboards($usecaseId: Int!) { usecaseDashboards(usecaseId: $usecaseId) { id @@ -26,7 +26,7 @@ const Dashboards = () => { const isValidId = !Number.isNaN(usecaseId); - const { data, isLoading } = useQuery<{ usecaseDashboards: any }>( + const { data, isLoading } = useQuery( ['fetch_dashboardData', usecaseId], () => GraphQL(DashboardsList, {}, { usecaseId }), { @@ -45,7 +45,7 @@ const Dashboards = () => { {isLoading ? ( ) : ( - data?.usecaseDashboards?.length > 0 && ( + (data?.usecaseDashboards?.length ?? 0) > 0 && (
@@ -56,7 +56,7 @@ const Dashboards = () => {
- {data?.usecaseDashboards?.map((dashboard: any) => ( + {data?.usecaseDashboards?.map((dashboard) => ( { data: UseCaseDetails, isLoading, error, - } = useQuery<{ useCase: TypeUseCase }>( + } = useQuery( [`fetch_UsecaseDetails_${params.useCaseSlug}`], async () => { - const result = (await GraphQLPublic( - UseCasedetails as any, - {}, - { - pk: params.useCaseSlug, - } - )) as { useCase: TypeUseCase }; + const result = await GraphQLPublic(UseCasedetails, {}, { + pk: + typeof params.useCaseSlug === 'string' ? params.useCaseSlug : '', + }); return result; }, { @@ -245,8 +241,9 @@ const UseCaseDetailClient = () => { Error Loading Use Case - {(error as any)?.message?.includes('401') || - (error as any)?.message?.includes('403') + {error instanceof Error && + (error.message.includes('401') || + error.message.includes('403')) ? 'You do not have permission to view this use case. Please log in or contact the administrator.' : 'Failed to load use case details. Please try again later.'} @@ -289,7 +286,7 @@ const UseCaseDetailClient = () => {
{/*
*/} {datasets.length > 0 && - datasets.map((dataset: TypeDataset) => ( + datasets.map((dataset) => ( { iconColor={'warning'} metadataContent={[ { - icon: Icons.calendarEvent as any, + icon: Icons.calendarEvent, label: 'Date', value: formatDate(dataset.modified) || '', stroke: 1.2, }, { - icon: Icons.fileDownload as any, + icon: Icons.fileDownload, label: 'Download', value: dataset.downloadCount.toString(), stroke: 1.2, }, { - icon: Icons.worldPin as any, + icon: Icons.worldPin, label: 'Geography', value: dataset.geographies && dataset.geographies.length > 0 ? dataset.geographies - .map((geo: any) => geo.name) + .map((geo) => geo.name) .join(', ') : '', stroke: 1.2, @@ -324,7 +321,7 @@ const UseCaseDetailClient = () => { href={`/datasets/${dataset.id}`} leftFooterChips={[ { - icon: `/Sectors/${dataset.sectors[0]?.name}.svg` as any, + icon: `/Sectors/${dataset.sectors[0]?.name}.svg`, label: 'Sectors', }, ]} @@ -360,7 +357,7 @@ const UseCaseDetailClient = () => {
{UseCaseDetails?.useCase?.supportingOrganizations?.map( - (org: any) => ( + (org) => ( {
{UseCaseDetails?.useCase?.partnerOrganizations?.map( - (org: any) => ( + (org) => ( {
{UseCaseDetails?.useCase?.contributors?.map( - (contributor: any) => ( + (contributor) => ( tag.value) || [], + keywords: UseCase?.tags?.map((tag) => tag.value) || [], openGraph: { type: 'article', locale: 'en_US', diff --git a/app/[locale]/(user)/usecases/components/Details.tsx b/app/[locale]/(user)/usecases/components/Details.tsx index c41ad53d..da3307bf 100644 --- a/app/[locale]/(user)/usecases/components/Details.tsx +++ b/app/[locale]/(user)/usecases/components/Details.tsx @@ -4,11 +4,17 @@ import React, { useState } from 'react'; import Image from 'next/image'; import { Button, Icon, Spinner, Tag, Text, Tray } from 'opub-ui'; +import { UseCasedetailsQuery } from '@/gql/generated/graphql'; import { Icons } from '@/components/icons'; import { RichTextRenderer } from '@/components/RichTextRenderer'; import Metadata from './Metadata'; -const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => { +interface PrimaryDetailsProps { + data: UseCasedetailsQuery; + isLoading: boolean; +} + +const PrimaryDetails = ({ data, isLoading }: PrimaryDetailsProps) => { const [open, setOpen] = useState(false); return ( @@ -17,7 +23,7 @@ const PrimaryDetails = ({ data, isLoading }: { data: any; isLoading: any }) => { {data.useCase.title}
- {data.useCase.tags.map((item: any, index: number) => ( + {data.useCase.tags?.map((item, index: number) => (
{
{
Geographies
- {data.useCase.geographies.map((geo: any, index: number) => ( + {data.useCase.geographies.map((geo, index: number) => ( {
)}
- +
diff --git a/app/[locale]/(user)/usecases/components/Metadata.tsx b/app/[locale]/(user)/usecases/components/Metadata.tsx index a4417e1c..3ef70a86 100644 --- a/app/[locale]/(user)/usecases/components/Metadata.tsx +++ b/app/[locale]/(user)/usecases/components/Metadata.tsx @@ -3,32 +3,44 @@ import Image from 'next/image'; import Link from 'next/link'; import { Button, Divider, Icon, Text, Tooltip } from 'opub-ui'; +import { UseCasedetailsQuery } from '@/gql/generated/graphql'; import { formatDate, getWebsiteTitle } from '@/lib/utils'; import { Icons } from '@/components/icons'; -const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { - const [platformTitle, setPlatformTitle] = useState(null); +interface MetadataProps { + data: UseCasedetailsQuery; + setOpen?: (isOpen: boolean) => void; +} + +const Metadata = ({ data, setOpen }: MetadataProps) => { + const platformUrl = data.useCase.platformUrl; + const [platformTitle, setPlatformTitle] = useState( + platformUrl === null ? 'N/A' : null + ); + const [prevPlatformUrl, setPrevPlatformUrl] = useState(platformUrl); + if (platformUrl !== prevPlatformUrl) { + setPrevPlatformUrl(platformUrl); + setPlatformTitle(platformUrl === null ? 'N/A' : null); + } useEffect(() => { - const fetchTitle = async () => { - try { - const urlItem = data.useCase.platformUrl; + if (!platformUrl) { + return; + } - if (urlItem && urlItem.value) { - const title = await getWebsiteTitle(urlItem.value); - setPlatformTitle(title); - } - } catch (error) { + let cancelled = false; + getWebsiteTitle(platformUrl) + .then((title) => { + if (!cancelled) setPlatformTitle(title); + }) + .catch((error) => { console.error('Error fetching website title:', error); - } - }; + }); - if (data.useCase.platformUrl === null) { - setPlatformTitle('N/A'); - } else { - fetchTitle(); - } - }, [data.useCase.platformUrl]); + return () => { + cancelled = true; + }; + }, [platformUrl]); const getOrganizationLink = () => { if (!data) return '/publishers'; @@ -52,7 +64,7 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { content={ data.useCase.isIndividualUsecase ? data.useCase.user.fullName - : data.useCase.organization.name + : data.useCase.organization?.name } > @@ -68,7 +80,7 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { value: ( Contact{' '} {data.useCase.isIndividualUsecase ? 'Publisher' : 'Organization'} @@ -83,7 +95,7 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { ) : ( {platformTitle?.trim() ? platformTitle : 'Visit Platform'} @@ -103,8 +115,9 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { : []), { label: 'Status', - value: data.useCase.runningStatus.split('_').join('') || 'N/A', - tooltipContent: data.useCase.runningStatus.split('_').join('') || 'N/A', + value: String(data.useCase.runningStatus).split('_').join('') || 'N/A', + tooltipContent: + String(data.useCase.runningStatus).split('_').join('') || 'N/A', }, { label: 'Last Updated', @@ -115,8 +128,8 @@ const Metadata = ({ data, setOpen }: { data: any; setOpen?: any }) => { label: 'Sectors', value: (
- {data.useCase.sectors.length > 0 ? ( - data.useCase.sectors.map((sector: any, index: number) => ( + {data.useCase.sectors && data.useCase.sectors.length > 0 ? ( + data.useCase.sectors.map((sector, index: number) => ( { value: (
{data.useCase.sdgs && data.useCase.sdgs.length > 0 ? ( - data.useCase.sdgs.map((sdg: any, index: number) => ( + data.useCase.sdgs.map((sdg, index: number) => ( void; + selectedUser: SelectedUser; + isOpen: boolean; + isEdit: boolean; + setRefetch: (refetch: boolean) => void; +} + const AddUser = ({ setIsOpen, selectedUser, isOpen, isEdit, setRefetch, -}: { - setIsOpen: (isOpen: boolean) => void; - selectedUser: any; - isOpen: boolean; - isEdit: boolean; - setRefetch: (refetch: boolean) => void; -}) => { - const [searchValue, setSearchValue] = useState(''); +}: AddUserProps) => { + const [searchValue, setSearchValue] = useState(selectedUser?.name || ''); + const [formData, setFormData] = useState({ + userId: selectedUser?.id || '', + roleId: selectedUser?.role?.id || '', + }); + const [prevSelectedUser, setPrevSelectedUser] = useState(selectedUser); + if (selectedUser !== prevSelectedUser) { + setPrevSelectedUser(selectedUser); + if (selectedUser) { + setSearchValue(selectedUser.name || ''); + setFormData({ + userId: selectedUser.id || '', + roleId: selectedUser.role?.id || '', + }); + } else { + setFormData({ userId: '', roleId: '' }); + setSearchValue(''); + } + } const params = useParams<{ entityType: string; entitySlug: string; id: string; }>(); - const Users: { data: any; isLoading: boolean; refetch: any } = useQuery( + const Users = useQuery( [`fetch_users_list`], () => GraphQL( @@ -91,31 +125,14 @@ const AddUser = ({ } ); - const RolesList: { data: any; isLoading: boolean; refetch: any } = useQuery( + const RolesList = useQuery( [`fetch_UseCaseData`], () => - GraphQL( - allRolesDoc, - { - [params.entityType]: params.entitySlug, - }, - [] - ) + GraphQL(allRolesDoc, { + [params.entityType]: params.entitySlug, + }) ); - useEffect(() => { - if (selectedUser) { - setSearchValue(selectedUser.name || ''); - setFormData({ - userId: selectedUser.id || '', - roleId: selectedUser.role?.id || '', - }); - } else { - setFormData({ userId: '', roleId: '' }); - setSearchValue(''); - } - }, [selectedUser]); - const { mutate } = useMutation( (input: { input: AddRemoveUserToOrganizationInput }) => GraphQL( @@ -126,7 +143,7 @@ const AddUser = ({ input ), { - onSuccess: (data: any) => { + onSuccess: (data) => { if (data.addUserToOrganization.success) { toast('User added successfully'); setIsOpen(false); @@ -141,7 +158,7 @@ const AddUser = ({ (data.addUserToOrganization?.errors?.fieldErrors ? data.addUserToOrganization?.errors?.fieldErrors[0] ?.messages[0] - : data.addUserToOrganization?.errors?.nonFieldErrors[0]) + : data.addUserToOrganization?.errors?.nonFieldErrors?.[0]) ); } }, @@ -173,11 +190,7 @@ const AddUser = ({ } ); - const [formData, setFormData] = useState({ - userId: '', - roleId: '', - }); - const handleChange = (field: string, value: any) => { + const handleChange = (field: 'userId' | 'roleId', value: string) => { setFormData((prev) => ({ ...prev, [field]: value, @@ -200,7 +213,7 @@ const AddUser = ({ Users.refetch(); // Refetch when search term changes }; - const handleSelectOption = (option: any) => { + const handleSelectOption = (option: SearchUserOption) => { handleChange('userId', option.id); setSearchValue(option.fullName); setIsDropdownOpen(false); // Close dropdown @@ -227,9 +240,9 @@ const AddUser = ({ className="border border-gray-100 placeholder:text-sm mt-1 block w-full px-3 py-1" placeholder={'Select user'} /> - {isDropdownOpen && filteredOptions?.length > 0 && ( + {isDropdownOpen && (filteredOptions?.length ?? 0) > 0 && (
- {filteredOptions.map((option: any) => ( + {filteredOptions?.map((option: SearchUserOption) => (
handleChange('roleId', e)} - options={RolesList.data?.roles - .filter((role: any) => role.name !== 'owner') - .map((role: any) => ({ - label: toTitleCase(role.name), - value: role.id, - }))} + options={ + RolesList.data?.roles + ?.filter((role) => role.name !== 'owner') + .map((role) => ({ + label: toTitleCase(role.name), + value: role.id, + })) ?? [] + } label="Select a role *" helpText={RolesList.data?.roles - .filter((role: any) => role.id === formData.roleId) - .map((role: any) => role.description)} + ?.filter((role) => role.id === formData.roleId) + .map((role) => role.description)} />
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/page.tsx index eae5a763..eaeb191b 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/admin/page.tsx @@ -11,9 +11,9 @@ import { GraphQL } from '@/lib/api'; import { formatDate } from '@/lib/utils'; import { Icons } from '@/components/icons'; import { Loading } from '@/components/loading'; -import AddUser from './addUser'; +import AddUser, { SelectedUser } from './addUser'; -const usersListDoc: any = graphql(` +const usersListDoc = graphql(` query userByOrg { userByOrganization { id @@ -30,7 +30,7 @@ const usersListDoc: any = graphql(` } `); -const removeUserDoc: any = graphql(` +const removeUserDoc = graphql(` mutation removeUserFromOrganization( $input: AddRemoveUserToOrganizationInput! ) { @@ -41,22 +41,32 @@ const removeUserDoc: any = graphql(` } `); +interface AdminTableRow { + name: string; + role: string; + roleId: string; + modified: string; + id: string; +} + +interface AdminTableCellProps { + row: { + original: AdminTableRow; + }; +} + const Admin = () => { const params = useParams<{ entityType: string; entitySlug: string }>(); - const usersList: { data: any; isLoading: boolean; refetch: any } = useQuery( + const usersList = useQuery( [`fetch_users_list_admin_members`], () => - GraphQL( - usersListDoc, - { - [params.entityType]: params.entitySlug, - }, - [] - ) + GraphQL(usersListDoc, { + [params.entityType]: params.entitySlug, + }) ); const [isOpen, setIsOpen] = useState(false); const [isEdit, setIsEdit] = useState(false); - const [selectedUser, setSelectedUser] = useState({}); + const [selectedUser, setSelectedUser] = useState({}); const [refetch, setRefetch] = useState(false); const { mutate } = useMutation( @@ -90,7 +100,7 @@ const Admin = () => { { accessorKey: 'edit', header: 'Edit', - cell: ({ row }: any) => ( + cell: ({ row }: AdminTableCellProps) => (
- {usersList.data?.userByOrganization?.length > 0 ? ( + {usersList.data?.userByOrganization && + usersList.data.userByOrganization.length > 0 ? ( ; + return { + targetUsers: + typeof record.targetUsers === 'string' ? record.targetUsers : '', + intendedUse: + typeof record.intendedUse === 'string' ? record.intendedUse : '', + modelWebsite: + typeof record.modelWebsite === 'string' ? record.modelWebsite : '', + usageLicense: + typeof record.usageLicense === 'string' ? record.usageLicense : '', + }; + } + return {}; +} + +function emptyAIModelForm(): AIModelFormData { + return { + name: '', + modelType: 'TEXT_GENERATION', + domain: '', + description: '', + targetUsers: '', + intendedUse: '', + sectors: [], + tags: [], + maxTokens: '', + supportedLanguages: [], + modelWebsite: '', + geographies: [], + usageLicense: '', + accessType: 'open', + }; +} + +function formDataFromModel(model: { + displayName?: string | null; + name?: string | null; + modelType?: string | null; + domain?: string | null; + description?: string | null; + metadata?: unknown; + sectors?: Array<{ id: string; name: string }> | null; + tags?: Array<{ id: string; value: string }> | null; + maxTokens?: number | null; + supportedLanguages?: unknown; + geographies?: Array<{ id: string; name: string }> | null; + isPublic?: boolean | null; +}): AIModelFormData { + const metadata = asModelMetadata(model.metadata); + return { + name: model.displayName || model.name || '', + modelType: model.modelType || 'TEXT_GENERATION', + domain: model.domain || '', + description: model.description || '', + targetUsers: metadata.targetUsers || '', + intendedUse: metadata.intendedUse || '', + sectors: + model.sectors?.map((s) => ({ label: s.name, value: s.id })) || [], + tags: model.tags?.map((t) => ({ label: t.value, value: t.id })) || [], + maxTokens: model.maxTokens?.toString() || '', + supportedLanguages: Array.isArray(model.supportedLanguages) + ? model.supportedLanguages + .filter((l): l is string => typeof l === 'string') + .map((l) => ({ + label: + LANGUAGE_OPTIONS.find((option) => option.value === l)?.label || + l, + value: l, + })) + : [], + modelWebsite: metadata.modelWebsite || '', + geographies: + model.geographies?.map((g) => ({ + label: g.name, + value: g.id, + })) || [], + usageLicense: metadata.usageLicense || '', + accessType: model.isPublic ? 'open' : 'restricted', + }; +} + +function toSelectOptions(value: string | SelectOption[]): SelectOption[] { + return Array.isArray(value) ? value : []; +} + +const tagsListQueryDoc = graphql(` query TagsList { tags { id @@ -29,7 +146,7 @@ const tagsListQueryDoc: any = graphql(` } `); -const sectorsListQueryDoc: any = graphql(` +const sectorsListQueryDoc = graphql(` query AIModelSectorsList { sectors { id @@ -38,7 +155,7 @@ const sectorsListQueryDoc: any = graphql(` } `); -const geographiesListQueryDoc: any = graphql(` +const geographiesListQueryDoc = graphql(` query AIModelGeographiesList { geographies { id @@ -53,7 +170,7 @@ const geographiesListQueryDoc: any = graphql(` } `); -const promptDomainEnumValuesQueryDoc: any = graphql(` +const promptDomainEnumValuesQueryDoc = graphql(` query PromptDomainEnum { __type(name: "PromptDomain") { enumValues { @@ -64,7 +181,7 @@ const promptDomainEnumValuesQueryDoc: any = graphql(` } `); -const FetchAIModelDetails: any = graphql(` +const FetchAIModelDetails = graphql(` query AIModelDetails($filters: AIModelFilter) { aiModels(filters: $filters) { id @@ -93,7 +210,7 @@ const FetchAIModelDetails: any = graphql(` } `); -const UpdateAIModelMutation: any = graphql(` +const UpdateAIModelMutation = graphql(` mutation updateAIModelDetails($input: UpdateAIModelInput!) { updateAiModel(input: $input) { success @@ -135,22 +252,7 @@ export default function AIModelDetailsPage() { const { setStatus } = useEditStatus(); const queryClient = useQueryClient(); - const [formData, setFormData] = useState({ - name: '', - modelType: 'TEXT_GENERATION', - domain: '', - description: '', - targetUsers: '', - intendedUse: '', - sectors: [] as Array<{ label: string; value: string }>, - tags: [] as Array<{ label: string; value: string }>, - maxTokens: '', - supportedLanguages: [] as Array<{ label: string; value: string }>, - modelWebsite: '', - geographies: [] as Array<{ label: string; value: string }>, - usageLicense: '', - accessType: 'open' as 'open' | 'restricted', - }); + const [formData, setFormData] = useState(emptyAIModelForm()); const [isTagsListUpdated, setIsTagsListUpdated] = useState(false); const SAVE_SUCCESS_TOAST_ID = 'ai-model-details-save-success'; @@ -166,54 +268,41 @@ export default function AIModelDetailsPage() { } }; - const getTagsList: { - data: any; - isLoading: boolean; - error: any; - refetch: any; - } = useQuery([`tags_list_query`], () => + const getTagsList = useQuery([`tags_list_query`], () => GraphQL( tagsListQueryDoc, { [params.entityType]: params.entitySlug, - }, - {} as any + } ) ); - const getSectorsList: { data: any; isLoading: boolean; error: any } = + const getSectorsList = useQuery([`sectors_list_query`], () => GraphQL( sectorsListQueryDoc, { [params.entityType]: params.entitySlug, - }, - {} as any + } ) ); - const getGeographiesList: { data: any; isLoading: boolean; error: any } = + const getGeographiesList = useQuery([`geographies_list_query`], () => GraphQL( geographiesListQueryDoc, { [params.entityType]: params.entitySlug, - }, - {} as any + } ) ); - const getPromptDomainEnumValues: { data: any; isLoading: boolean; error: any } = + const getPromptDomainEnumValues = useQuery([`prompt_domain_enum_values_query`], () => - GraphQL(promptDomainEnumValuesQueryDoc, {}, [] as any) + GraphQL(promptDomainEnumValuesQueryDoc, {}) ); - const AIModelData: { - data: any; - isLoading: boolean; - refetch: any; - error: any; - } = useQuery( + const AIModelData = useQuery( [ `fetch_AIModelDetails`, params.id, @@ -241,7 +330,7 @@ export default function AIModelDetailsPage() { const model = AIModelData.data?.aiModels?.[0]; const { mutate } = useMutation( - (data: any) => + (data: Omit) => GraphQL( UpdateAIModelMutation, { @@ -280,9 +369,9 @@ export default function AIModelDetailsPage() { ], }); }, - onError: (error: any) => { + onError: (error: unknown) => { const errorMessage = - typeof error?.message === 'string' && error.message.trim() + typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' && error.message.trim() ? error.message.trim() : 'Unable to update AI Model right now. Please try again.'; toast(`Error: ${errorMessage}`, { id: SAVE_ERROR_TOAST_ID }); @@ -291,60 +380,26 @@ export default function AIModelDetailsPage() { } ); - useEffect(() => { - setFormData({ - name: '', - modelType: 'TEXT_GENERATION', - domain: '', - description: '', - targetUsers: '', - intendedUse: '', - sectors: [], - tags: [], - maxTokens: '', - supportedLanguages: [], - modelWebsite: '', - geographies: [], - usageLicense: '', - accessType: 'open', - }); - }, [params.id]); - - useEffect(() => { + const [prevId, setPrevId] = useState(params.id); + if (params.id !== prevId) { + setPrevId(params.id); + setFormData(emptyAIModelForm()); + } + + const [prevModel, setPrevModel] = useState( + undefined + ); + if (model !== prevModel) { + setPrevModel(model); if (model) { - const metadata = model.metadata || {}; - setFormData({ - name: model.displayName || model.name || '', - modelType: model.modelType || 'TEXT_GENERATION', - domain: model.domain || '', - description: model.description || '', - targetUsers: metadata.targetUsers || '', - intendedUse: metadata.intendedUse || '', - sectors: - model.sectors?.map((s: any) => ({ label: s.name, value: s.id })) || - [], - tags: - model.tags?.map((t: any) => ({ label: t.value, value: t.id })) || [], - maxTokens: model.maxTokens?.toString() || '', - supportedLanguages: - model.supportedLanguages?.map((l: string) => ({ - label: - LANGUAGE_OPTIONS.find((option) => option.value === l)?.label || l, - value: l, - })) || [], - modelWebsite: metadata.modelWebsite || '', - geographies: - model.geographies?.map((g: any) => ({ - label: g.name, - value: g.id, - })) || [], - usageLicense: metadata.usageLicense || '', - accessType: model.isPublic ? 'open' : 'restricted', - }); + setFormData(formDataFromModel(model)); } - }, [model]); + } - const handleInputChange = (field: string, value: any) => { + const handleInputChange = ( + field: keyof AIModelFormData, + value: AIModelFormData[keyof AIModelFormData] + ) => { console.log('handleInputChange', field, value); setFormData((prev) => ({ ...prev, [field]: value })); setStatus('unsaved'); @@ -375,7 +430,7 @@ export default function AIModelDetailsPage() { handleSave({ ...formData, modelWebsite: trimmedWebsite }); }; - const handleSave = (overrideData?: any) => { + const handleSave = (overrideData?: AIModelFormData) => { setStatus('saving'); const dataToUse = overrideData || formData; @@ -388,17 +443,25 @@ export default function AIModelDetailsPage() { return; } - const updateData: any = { + const updateData: Omit = { description: dataToUse.description, - modelType: dataToUse.modelType, - domain: dataToUse.domain || null, - tags: dataToUse.tags.map((item: any) => item.label), - sectors: dataToUse.sectors.map((item: any) => item.label), - geographies: dataToUse.geographies.map((item: any) => + modelType: (Object.values(AiModelType) as string[]).includes( + dataToUse.modelType + ) + ? (dataToUse.modelType as AiModelType) + : AiModelType.TextGeneration, + domain: + dataToUse.domain && + (Object.values(PromptDomain) as string[]).includes(dataToUse.domain) + ? (dataToUse.domain as PromptDomain) + : null, + tags: dataToUse.tags.map((item) => item.label), + sectors: dataToUse.sectors.map((item) => item.label), + geographies: dataToUse.geographies.map((item) => parseInt(item.value, 10) ), supportedLanguages: dataToUse.supportedLanguages.map( - (item: any) => item.value + (item) => item.value ), maxTokens: parseInt(dataToUse.maxTokens) || null, isPublic: dataToUse.accessType === 'open', @@ -544,7 +607,7 @@ export default function AIModelDetailsPage() { displaySelected name="sectors" list={ - getSectorsList.data?.sectors?.map((item: any) => ({ + getSectorsList.data?.sectors?.map((item) => ({ label: item.name, value: item.id, })) || [] @@ -553,8 +616,9 @@ export default function AIModelDetailsPage() { label="Sectors" selectedValue={formData.sectors || []} onChange={(value) => { - handleInputChange('sectors', value); - handleSave({ ...formData, sectors: value }); + const next = toSelectOptions(value); + handleInputChange('sectors', next); + handleSave({ ...formData, sectors: next }); }} required requiredIndicator={true} @@ -565,7 +629,7 @@ export default function AIModelDetailsPage() { displaySelected name="tags" list={ - getTagsList.data?.tags?.map((item: any) => ({ + getTagsList.data?.tags?.map((item) => ({ label: item.value, value: item.id, })) || [] @@ -576,9 +640,10 @@ export default function AIModelDetailsPage() { selectedValue={formData.tags || []} requiredIndicator onChange={(value) => { + const next = toSelectOptions(value); setIsTagsListUpdated(true); - handleInputChange('tags', value); - handleSave({ ...formData, tags: value }); + handleInputChange('tags', next); + handleSave({ ...formData, tags: next }); }} /> @@ -605,8 +670,9 @@ export default function AIModelDetailsPage() { key={`languages-${formData.supportedLanguages.length}`} selectedValue={formData.supportedLanguages || []} onChange={(value) => { - handleInputChange('supportedLanguages', value); - handleSave({ ...formData, supportedLanguages: value }); + const next = toSelectOptions(value); + handleInputChange('supportedLanguages', next); + handleSave({ ...formData, supportedLanguages: next }); }} required requiredIndicator={true} @@ -631,7 +697,7 @@ export default function AIModelDetailsPage() { displaySelected name="geographies" list={ - getGeographiesList.data?.geographies?.map((item: any) => ({ + getGeographiesList.data?.geographies?.map((item) => ({ label: `${item.name}${item.parentId ? ` (${item.parentId.name})` : ''}`, value: item.id, })) || [] @@ -641,8 +707,9 @@ export default function AIModelDetailsPage() { requiredIndicator selectedValue={formData.geographies || []} onChange={(value) => { - handleInputChange('geographies', value); - handleSave({ ...formData, geographies: value }); + const next = toSelectOptions(value); + handleInputChange('geographies', next); + handleSave({ ...formData, geographies: next }); }} /> diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx index 59a4dbbc..67e4315f 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx @@ -2,6 +2,7 @@ import { useParams, useRouter } from 'next/navigation'; import { graphql } from '@/gql'; +import { AiModelStatus, UpdateAiModelInput } from '@/gql/generated/graphql'; import { useMutation, useQuery } from '@tanstack/react-query'; import { Accordion, @@ -22,7 +23,7 @@ import { Icons } from '@/components/icons'; import { RichTextRenderer } from '@/components/RichTextRenderer'; import { useEditStatus } from '../../context'; -const FetchAIModelForPublish: any = graphql(` +const FetchAIModelForPublish = graphql(` query AIModelForPublish($filters: AIModelFilter) { aiModels(filters: $filters) { id @@ -65,7 +66,7 @@ const FetchAIModelForPublish: any = graphql(` } `); -const UpdateAIModelStatusMutation: any = graphql(` +const UpdateAIModelStatusMutation = graphql(` mutation updateAIModelStatus($input: UpdateAIModelInput!) { updateAiModel(input: $input) { success @@ -175,15 +176,15 @@ export default function PublishPage() { } ); - const model = (data as any)?.aiModels?.[0]; + const model = data?.aiModels?.[0]; const versions = model?.versions || []; - const primaryVersion = versions.find((v: any) => v.isLatest) || versions[0]; - const hasProviders = versions.some((v: any) => v.providers?.length > 0); + const primaryVersion = versions.find((v) => v.isLatest) || versions[0]; + const hasProviders = versions.some((v) => v.providers?.length > 0); const PUBLISH_SUCCESS_TOAST_ID = 'publish-ai-model-success'; const PUBLISH_ERROR_TOAST_ID = 'publish-ai-model-error'; const { mutate, isLoading: updateLoading } = useMutation( - (mutationData: any) => + (mutationData: Pick) => GraphQL( UpdateAIModelStatusMutation, { @@ -203,7 +204,7 @@ export default function PublishPage() { setStatus('saving'); mutate( { - status: isPublished ? 'REGISTERED' : 'ACTIVE', + status: isPublished ? AiModelStatus.Registered : AiModelStatus.Active, isPublic: isPublished ? false : true, isActive: isPublished ? false : true, }, @@ -223,9 +224,9 @@ export default function PublishPage() { `/dashboard/${params.entityType}/${params.entitySlug}/aimodels?tab=${isPublished ? 'draft' : 'published'}` ); }, - onError: (error: any) => { + onError: (error: unknown) => { const errorMessage = - typeof error?.message === 'string' && error.message.trim() + typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' && error.message.trim() ? error.message.trim() : isPublished ? 'Unable to unpublish model right now. Please try again.' @@ -290,12 +291,12 @@ export default function PublishPage() { { accessorKey: 'primary', header: 'Primary' }, ]; - const versionRows = versions.map((v: any) => ({ + const versionRows = versions.map((v) => ({ version: v.version, lifecycleStage: lifecycleLabels[v.lifecycleStage] || v.lifecycleStage, providers: v.providers?.length ? v.providers - .map((p: any) => providerLabels[p.provider] || p.provider) + .map((p) => providerLabels[p.provider] || p.provider) .join(', ') : 'None', primary: v.isLatest ? 'Yes' : 'No', @@ -309,7 +310,10 @@ export default function PublishPage() { }, { label: 'Model Type', - value: modelTypeLabels[model?.modelType] || model?.modelType || '', + value: + (model?.modelType && modelTypeLabels[model.modelType]) || + model?.modelType || + '', }, { label: 'Domain', @@ -437,8 +441,8 @@ export default function PublishPage() { Sectors:
- {model?.sectors?.length > 0 ? ( - model.sectors.map((s: any, idx: number) => ( + {(model?.sectors?.length ?? 0) > 0 ? ( + model?.sectors?.map((s, idx: number) => ( {s.name} )) ) : ( @@ -454,8 +458,8 @@ export default function PublishPage() { Tags:
- {model?.tags?.length > 0 ? ( - model.tags.map((t: any, idx: number) => ( + {(model?.tags?.length ?? 0) > 0 ? ( + model?.tags?.map((t, idx: number) => ( {t.value} )) ) : ( @@ -471,9 +475,9 @@ export default function PublishPage() { Geographies:
- {model?.geographies?.length > 0 ? ( - model.geographies.map( - (g: any, idx: number) => ( + {(model?.geographies?.length ?? 0) > 0 ? ( + model?.geographies?.map( + (g, idx: number) => ( {g.name} ) ) diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/versions/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/versions/page.tsx index caba7c8f..b3356b66 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/versions/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/versions/page.tsx @@ -3,6 +3,16 @@ import { useState } from 'react'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; +import { + AiModelLifecycleStage, + AiModelProvider, + CreateAiModelVersionInput, + CreateVersionProviderInput, + EndpointAuthType, + EndpointHttpMethod, + UpdateAiModelVersionInput, + UpdateVersionProviderInput, +} from '@/gql/generated/graphql'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button, @@ -24,7 +34,7 @@ import { import { GraphQL } from '@/lib/api'; import { Icons } from '@/components/icons'; -const fetchModelVersions: any = graphql(` +const fetchModelVersions = graphql(` query FetchModelVersions($filters: AIModelFilter) { aiModels(filters: $filters) { id @@ -76,7 +86,7 @@ const fetchModelVersions: any = graphql(` } `); -const createVersionMutation: any = graphql(` +const createVersionMutation = graphql(` mutation CreateNewModelVersion($input: CreateAIModelVersionInput!) { createAiModelVersion(input: $input) { success @@ -89,7 +99,7 @@ const createVersionMutation: any = graphql(` } `); -const updateVersionMutation: any = graphql(` +const updateVersionMutation = graphql(` mutation UpdateModelVersion($input: UpdateAIModelVersionInput!) { updateAiModelVersion(input: $input) { success @@ -102,7 +112,7 @@ const updateVersionMutation: any = graphql(` } `); -const createProviderMutation: any = graphql(` +const createProviderMutation = graphql(` mutation CreateModelVersionProvider($input: CreateVersionProviderInput!) { createVersionProvider(input: $input) { success @@ -116,7 +126,7 @@ const createProviderMutation: any = graphql(` } `); -const updateProviderMutation: any = graphql(` +const updateProviderMutation = graphql(` mutation UpdateModelVersionProvider($input: UpdateVersionProviderInput!) { updateVersionProvider(input: $input) { success @@ -130,7 +140,7 @@ const updateProviderMutation: any = graphql(` } `); -const deleteProviderMutation: any = graphql(` +const deleteProviderMutation = graphql(` mutation DeleteModelVersionProvider($providerId: Int!) { deleteVersionProvider(providerId: $providerId) { success @@ -138,6 +148,48 @@ const deleteProviderMutation: any = graphql(` } `); +interface VersionProviderRow { + id: number; + provider: string; + providerModelId?: string | null; + isPrimary: boolean; + isActive?: boolean; + apiEndpointUrl?: string | null; + apiHttpMethod?: string | null; + apiTimeoutSeconds?: number | null; + apiAuthType?: string | null; + apiAuthHeaderName?: string | null; + apiKey?: string | null; + apiKeyPrefix?: string | null; + apiHeaders?: Record | null; + apiRequestTemplate?: unknown; + apiResponsePath?: string | null; + hfUsePipeline?: boolean | null; + hfAuthToken?: string | null; + hfModelClass?: string | null; + hfAttnImplementation?: string | null; + hfTrustRemoteCode?: boolean | null; + hfTorchDtype?: string | null; + hfDeviceMap?: string | null; + framework?: string | null; +} + +interface ModelVersionRow { + id: number; + version: string; + versionNotes?: string | null; + status?: string | null; + lifecycleStage?: AiModelLifecycleStage | null; + isLatest: boolean; + supportsStreaming?: boolean; + maxTokens?: number | null; + supportedLanguages?: unknown; + createdAt?: string | null; + updatedAt?: string | null; + publishedAt?: string | null; + providers: VersionProviderRow[]; +} + export default function VersionsPage() { const params = useParams<{ entityType: string; @@ -170,29 +222,32 @@ export default function VersionsPage() { const [isWhatsThisModalOpen, setIsWhatsThisModalOpen] = useState(false); const [isPrimaryConfirmModalOpen, setIsPrimaryConfirmModalOpen] = useState(false); - const [selectedVersion, setSelectedVersion] = useState(null); - const [editingProvider, setEditingProvider] = useState(null); + const [selectedVersion, setSelectedVersion] = useState( + null + ); + const [editingProvider, setEditingProvider] = + useState(null); const [pendingPrimaryVersionId, setPendingPrimaryVersionId] = useState< number | null >(null); const [newVersionData, setNewVersionData] = useState({ version: '', - lifecycleStage: 'DEVELOPMENT', + lifecycleStage: AiModelLifecycleStage.Development, copyFromVersionId: null as number | null, isLatest: false, }); const [providerFormData, setProviderFormData] = useState({ - provider: 'CUSTOM', + provider: AiModelProvider.Custom, providerModelId: '', isPrimary: false, // API Endpoint Configuration apiEndpointUrl: '', - apiHttpMethod: 'POST', + apiHttpMethod: EndpointHttpMethod.Post, apiTimeoutSeconds: 60, // Authentication Configuration - apiAuthType: 'BEARER', + apiAuthType: EndpointAuthType.Bearer, apiAuthHeaderName: 'Authorization', apiKey: '', apiKeyPrefix: 'Bearer', @@ -219,7 +274,7 @@ export default function VersionsPage() { () => GraphQL(fetchModelVersions, { [params.entityType]: params.entitySlug }, { filters: { id: parseInt(params.id) }, - } as any), + }), { enabled: !!params.id, refetchOnMount: true, @@ -227,20 +282,20 @@ export default function VersionsPage() { } ); - const model = (data as any)?.aiModels?.[0]; + const model = data?.aiModels?.[0]; const versions = model?.versions || []; - const latestVersion = versions.find((v: any) => v.isLatest) || versions[0]; + const latestVersion = versions.find((v) => v.isLatest) || versions[0]; // Mutations const { mutate: createVersion, isLoading: createLoading } = useMutation( - (input: any) => + (input: CreateAiModelVersionInput) => GraphQL( createVersionMutation, { [params.entityType]: params.entitySlug }, { input } ), { - onSuccess: async (response: any) => { + onSuccess: async (response) => { toast('New version created successfully!',{id: VERSIONS_ACTION_TOAST_ID}); setIsNewVersionModalOpen(false); resetVersionForm(); @@ -252,24 +307,24 @@ export default function VersionsPage() { if (newVersionId && result.data) { const refetchedVersions = - (result.data as any)?.aiModels?.[0]?.versions || []; + result.data?.aiModels?.[0]?.versions || []; const newVersion = refetchedVersions.find( - (v: any) => v.id === newVersionId + (v) => v.id === newVersionId ); if (newVersion) { setSelectedVersion(newVersion); } } }, - onError: (error: any) => { - toast(`Error: ${error.message}`,{id: VERSIONS_ACTION_TOAST_ID}); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`,{id: VERSIONS_ACTION_TOAST_ID}); }, } ); const { mutate: createProvider, isLoading: createProviderLoading } = useMutation( - (input: any) => + (input: CreateVersionProviderInput) => GraphQL( createProviderMutation, { [params.entityType]: params.entitySlug }, @@ -286,23 +341,23 @@ export default function VersionsPage() { const result = await refetch(); if (result.data && selectedVersion) { const refetchedVersions = - (result.data as any)?.aiModels?.[0]?.versions || []; + result.data?.aiModels?.[0]?.versions || []; const updatedVersion = refetchedVersions.find( - (v: any) => v.id === selectedVersion.id + (v) => v.id === selectedVersion.id ); if (updatedVersion) { setSelectedVersion(updatedVersion); } } }, - onError: (error: any) => { - toast(`Error: ${error.message}`,{id: VERSIONS_ACTION_TOAST_ID}); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`,{id: VERSIONS_ACTION_TOAST_ID}); }, } ); const { mutate: updateProvider, isLoading: updateProviderLoading } = useMutation( - (input: any) => + (input: UpdateVersionProviderInput) => GraphQL( updateProviderMutation, { [params.entityType]: params.entitySlug }, @@ -320,17 +375,17 @@ export default function VersionsPage() { const result = await refetch(); if (result.data && selectedVersion) { const refetchedVersions = - (result.data as any)?.aiModels?.[0]?.versions || []; + result.data?.aiModels?.[0]?.versions || []; const updatedVersion = refetchedVersions.find( - (v: any) => v.id === selectedVersion.id + (v) => v.id === selectedVersion.id ); if (updatedVersion) { setSelectedVersion(updatedVersion); } } }, - onError: (error: any) => { - toast(`Error: ${error.message}`,{id: VERSIONS_ACTION_TOAST_ID}); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`,{id: VERSIONS_ACTION_TOAST_ID}); }, } ); @@ -340,7 +395,7 @@ export default function VersionsPage() { GraphQL( deleteProviderMutation, { [params.entityType]: params.entitySlug }, - { providerId } as any + { providerId } ), { onSuccess: async () => { @@ -351,17 +406,17 @@ export default function VersionsPage() { const result = await refetch(); if (result.data && selectedVersion) { const refetchedVersions = - (result.data as any)?.aiModels?.[0]?.versions || []; + result.data?.aiModels?.[0]?.versions || []; const updatedVersion = refetchedVersions.find( - (v: any) => v.id === selectedVersion.id + (v) => v.id === selectedVersion.id ); if (updatedVersion) { setSelectedVersion(updatedVersion); } } }, - onError: (error: any) => { - toast(`Error: ${error.message}`,{id: VERSIONS_ACTION_TOAST_ID}); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`,{id: VERSIONS_ACTION_TOAST_ID}); }, } ); @@ -369,7 +424,7 @@ export default function VersionsPage() { const resetVersionForm = () => { setNewVersionData({ version: '', - lifecycleStage: 'DEVELOPMENT', + lifecycleStage: AiModelLifecycleStage.Development, copyFromVersionId: null, isLatest: false, }); @@ -377,15 +432,15 @@ export default function VersionsPage() { const resetProviderForm = () => { setProviderFormData({ - provider: 'CUSTOM', + provider: AiModelProvider.Custom, providerModelId: '', isPrimary: false, // API Endpoint Configuration apiEndpointUrl: '', - apiHttpMethod: 'POST', + apiHttpMethod: EndpointHttpMethod.Post, apiTimeoutSeconds: 60, // Authentication Configuration - apiAuthType: 'BEARER', + apiAuthType: EndpointAuthType.Bearer, apiAuthHeaderName: 'Authorization', apiKey: '', apiKeyPrefix: 'Bearer', @@ -418,7 +473,7 @@ export default function VersionsPage() { setNewVersionData({ version: suggestedVersion, - lifecycleStage: 'DEVELOPMENT', + lifecycleStage: AiModelLifecycleStage.Development, copyFromVersionId: latestVersion?.id || null, isLatest: false, }); @@ -438,26 +493,41 @@ export default function VersionsPage() { createVersion({ modelId: parseInt(params.id), version: newVersionData.version, - lifecycleStage: newVersionData.lifecycleStage, + lifecycleStage: newVersionData.lifecycleStage || AiModelLifecycleStage.Development, copyFromVersionId: newVersionData.copyFromVersionId, isLatest: newVersionData.isLatest, }); }; - const handleOpenProviderModal = (version: any, provider?: any) => { + const handleOpenProviderModal = ( + version: ModelVersionRow, + provider?: VersionProviderRow + ) => { setSelectedVersion(version); if (provider) { setEditingProvider(provider); setProviderFormData({ - provider: provider.provider, + provider: (Object.values(AiModelProvider) as string[]).includes( + provider.provider + ) + ? (provider.provider as AiModelProvider) + : AiModelProvider.Custom, providerModelId: provider.providerModelId || '', isPrimary: provider.isPrimary, // API Endpoint Configuration apiEndpointUrl: provider.apiEndpointUrl || '', - apiHttpMethod: provider.apiHttpMethod || 'POST', + apiHttpMethod: (Object.values(EndpointHttpMethod) as string[]).includes( + provider.apiHttpMethod ?? '' + ) + ? (provider.apiHttpMethod as EndpointHttpMethod) + : EndpointHttpMethod.Post, apiTimeoutSeconds: provider.apiTimeoutSeconds || 60, // Authentication Configuration - apiAuthType: provider.apiAuthType || 'BEARER', + apiAuthType: (Object.values(EndpointAuthType) as string[]).includes( + provider.apiAuthType ?? '' + ) + ? (provider.apiAuthType as EndpointAuthType) + : EndpointAuthType.Bearer, apiAuthHeaderName: provider.apiAuthHeaderName || 'Authorization', apiKey: provider.apiKey || '', apiKeyPrefix: provider.apiKeyPrefix || 'Bearer', @@ -539,10 +609,10 @@ export default function VersionsPage() { isPrimary: providerFormData.isPrimary, // API Endpoint Configuration apiEndpointUrl: providerFormData.apiEndpointUrl || null, - apiHttpMethod: providerFormData.apiHttpMethod || 'POST', + apiHttpMethod: providerFormData.apiHttpMethod || EndpointHttpMethod.Post, apiTimeoutSeconds: providerFormData.apiTimeoutSeconds, // Authentication Configuration - apiAuthType: providerFormData.apiAuthType || 'BEARER', + apiAuthType: providerFormData.apiAuthType || EndpointAuthType.Bearer, apiAuthHeaderName: providerFormData.apiAuthHeaderName || 'Authorization', apiKey: providerFormData.apiKey || null, apiKeyPrefix: providerFormData.apiKeyPrefix || 'Bearer', @@ -617,7 +687,7 @@ export default function VersionsPage() { ]; const { mutate: updateVersion } = useMutation( - (input: any) => + (input: UpdateAiModelVersionInput) => GraphQL( updateVersionMutation, { [params.entityType]: params.entitySlug }, @@ -629,18 +699,27 @@ export default function VersionsPage() { refetch(); invalidateVersionQueries(); }, - onError: (error: any) => { - toast(`Error: ${error.message}`,{id: VERSIONS_ACTION_TOAST_ID}); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`,{id: VERSIONS_ACTION_TOAST_ID}); }, } ); - const handleLifecycleChange = (versionId: number, lifecycleStage: string) => { + const handleLifecycleChange = ( + versionId: number, + lifecycleStage: string + ) => { + const stage = ( + Object.values(AiModelLifecycleStage) as string[] + ).includes(lifecycleStage) + ? (lifecycleStage as AiModelLifecycleStage) + : undefined; + if (!stage) return; const currentVersion = selectedVersion || latestVersion; if (currentVersion?.id === versionId) { - setSelectedVersion({ ...currentVersion, lifecycleStage }); + setSelectedVersion({ ...currentVersion, lifecycleStage: stage }); } - updateVersion({ id: versionId, lifecycleStage }); + updateVersion({ id: versionId, lifecycleStage: stage }); }; const handleSetPrimaryVersion = (versionId: number, isLatest: boolean) => { @@ -681,7 +760,7 @@ export default function VersionsPage() { return names[provider] || provider; }; - const getEndpointUrl = (provider: any) => { + const getEndpointUrl = (provider: VersionProviderRow) => { if (provider.apiEndpointUrl) { return provider.apiEndpointUrl; } @@ -694,7 +773,7 @@ export default function VersionsPage() { return provider.providerModelId || '-'; }; - const getAccessPriority = (provider: any) => { + const getAccessPriority = (provider: { isPrimary?: boolean }) => { return provider.isPrimary ? 'Primary Source' : 'Alternate Source'; }; @@ -714,7 +793,7 @@ export default function VersionsPage() { { - return { - label: item.title, - value: item.id, - }; - } - )} + options={ + getAllDatasetsWithResourcesRes?.data?.datasets?.map( + (item) => { + const option: SelectOption = { + label: item.title, + value: String(item.id), + }; + return option; + } + ) ?? [] + } required requiredIndicator={true} defaultValue={chartData?.dataset?.id} onChange={(e) => { - if ( + const dataset = getAllDatasetsWithResourcesRes?.data?.datasets?.find( - (ds: any) => ds.id === e - )?.resources?.length > 0 + (ds) => ds.id === e + ); + const firstResourceId = dataset?.resources?.[0]?.id; + if ( + (dataset?.resources?.length ?? 0) > 0 && + firstResourceId ) { setSelectedDataset(e); - handleSave( - 'resource', - getAllDatasetsWithResourcesRes?.data?.datasets?.find( - (ds: any) => ds.id === e - )?.resources[0].id - ); + handleSave('resource', String(firstResourceId)); } else { toast.error('No Resources found for this dataset'); } @@ -760,14 +891,20 @@ const ChartGenVizPreview = ({ params }: { params: any }) => { { - return { - label: item.fieldName, - value: item.id, - }; - })} + options={ + chartData?.resource?.schema?.map((item) => { + const option: SelectOption = { + label: item.fieldName, + value: item.id, + }; + return option; + }) ?? [] + } value={chartData.options?.xAxisColumn} onChange={(e) => { handleSave('xAxisColumn', e); @@ -815,7 +955,7 @@ const ChartGenVizPreview = ({ params }: { params: any }) => {
{chartData.options?.yAxisColumn?.map( - (columnItem: any, colIndex: number) => ( + (columnItem, colIndex: number) => (
- {chartImagesList?.datasetResourceCharts.map( - (item: any, index: any) => ( + {chartImagesList?.datasetResourceCharts?.map( + (item: ChartImageListItem, index: number) => (
= ({ /> = ({ actionHint="Accepts .gif, .jpg, and .png" actionTitle={ chartImageDetails && - chartImageDetails?.resourceChartImages[0]?.image?.name - .split('/') + chartImageDetails.resourceChartImages?.[0]?.image?.name + ?.split('/') .pop() } /> diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx index d37a133a..fc1fb36d 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsList.tsx @@ -1,4 +1,3 @@ -import { UUID } from 'crypto'; import React, { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { graphql } from '@/gql'; @@ -19,12 +18,38 @@ import { Icons } from '@/components/icons'; import ChartEditor from './ChartEditor'; interface ChartsListProps { - setType: any; - setChartId: any; - setImageId: any; + setType: (type: string) => void; + setChartId: (id: string | null) => void; + setImageId: (id: string | null) => void; } -const getAllCharts: any = graphql(` +interface ChartListEntry { + __typename: string; + name: string; + id: string; + chartType?: string; + status?: string; + dataset?: { title?: string; id?: string } | null; + resource?: { name?: string } | null; +} + +interface ChartTableRow { + name: string; + type: string; + id: string; + resource: string; + dataset: string; + typename: string; + status: string; +} + +interface ChartTableCellContext { + row: { + original: ChartTableRow; + }; +} + +const getAllCharts = graphql(` query ChartList { getChartData { __typename @@ -56,52 +81,18 @@ const getAllCharts: any = graphql(` } `); -const deleteResourceChart: any = graphql(` +const deleteResourceChart = graphql(` mutation deleteResourceChart($chartId: UUID!) { deleteResourceChart(chartId: $chartId) } `); -const deleteResourceChartImage: any = graphql(` +const deleteResourceChartImage = graphql(` mutation deleteResourceChartImage($resourceChartImageId: UUID!) { deleteResourceChartImage(resourceChartImageId: $resourceChartImageId) } `); -// const AddResourceChartImage: any = graphql(` -// mutation GenerateResourceChartImage($dataset: UUID!) { -// addResourceChartImage(dataset: $dataset) { -// __typename -// ... on TypeResourceChartImage { -// id -// name -// } -// } -// } -// `); - -// const AddResourceChart: any = graphql(` -// mutation GenerateResourceChart($resource: UUID!) { -// addResourceChart(resource: $resource) { -// __typename -// ... on TypeResourceChart { -// id -// name -// } -// } -// } -// `); - -// const datasetResourceList: any = graphql(` -// query all_resources($datasetId: UUID!) { -// datasetResources(datasetId: $datasetId) { -// id -// type -// name -// } -// } -// `); - const ChartsList: React.FC = () => { const params = useParams<{ entityType: string; @@ -113,140 +104,73 @@ const ChartsList: React.FC = () => { const [editorView, setEditorView] = useState(false); - const chartListRes: { - data: any; - isLoading: boolean; - refetch: any; - error: any; - isError: boolean; - } = useQuery([`chartList`], () => + const chartListRes = useQuery([`chartList`], () => GraphQL( getAllCharts, - params.entityType !== 'self' ? { - [params.entityType]: params.entitySlug, - } : {}, - [] + params.entityType !== 'self' + ? { + [params.entityType]: params.entitySlug, + } + : {} ) ); - const [filteredRows, setFilteredRows] = useState([]); - - useEffect(() => { - chartListRes.refetch(); + const [filteredRows, setFilteredRows] = useState([]); + const [prevChartListData, setPrevChartListData] = useState( + chartListRes.data?.getChartData + ); + if (chartListRes.data?.getChartData !== prevChartListData) { + setPrevChartListData(chartListRes.data?.getChartData); if (chartListRes.data?.getChartData) { setFilteredRows(chartListRes.data.getChartData); } - }, [chartListRes.data, chartListRes]); + } - const deleteResourceChartmutation: { mutate: any; isLoading: any } = - useMutation( - (data: { chartId: UUID }) => - GraphQL( - deleteResourceChart, - { - [params.entityType]: params.entitySlug, - }, - data - ), - { - onSuccess: () => { - toast('Chart Deleted Successfully'); - chartListRes.refetch(); - }, - onError: (err: any) => { - toast(`Received ${err} while deleting chart `); - }, - } - ); + useEffect(() => { + chartListRes.refetch(); + }, [chartListRes]); - const deleteResourceChartImagemutation: { mutate: any; isLoading: any } = - useMutation( - (data: { resourceChartImageId: string }) => - GraphQL( - deleteResourceChartImage, - { - [params.entityType]: params.entitySlug, - }, - data - ), - { - onSuccess: () => { - toast('ChartImage Deleted Successfully'); - chartListRes.refetch(); - }, - onError: (err: any) => { - toast(`Received ${err} while deleting chart `); + const deleteResourceChartmutation = useMutation( + (data: { chartId: string }) => + GraphQL( + deleteResourceChart, + { + [params.entityType]: params.entitySlug, }, - } - ); - - // const resourceChartImageMutation: { - // mutate: any; - // isLoading: any; - // } = useMutation( - // (data: { dataset: UUID }) => - // GraphQL( - // AddResourceChartImage, - // { - // [params.entityType]: params.entitySlug, - // }, - // data - // ), - // { - // onSuccess: (res: any) => { - // toast('Resource ChartImage Created Successfully'); - // chartListRes.refetch(); - // setType('img'); - // setImageId(res.addResourceChartImage.id); - - // // setImageId(res.id); - // }, - // onError: (err: any) => { - // toast(`Received ${err} while deleting chart `); - // }, - // } - // ); - - // AddResourceImage - - // const resourceList: { data: any } = useQuery([`charts_${params.id}`], () => - // GraphQL( - // datasetResourceList, - // { - // [params.entityType]: params.entitySlug, - // }, - // { datasetId: params.id } - // ) - // ); - - // const resourceChart: { - // mutate: any; - // isLoading: any; - // } = useMutation( - // (data: { resource: UUID }) => - // GraphQL( - // AddResourceChart, - // { - // [params.entityType]: params.entitySlug, - // }, - // data - // ), - // { - // onSuccess: (res: any) => { - // toast('Resource Chart Created Successfully'); - // chartListRes.refetch(); - // setType('visualize'); - // setChartId(res.addResourceChart.id); + data + ), + { + onSuccess: () => { + toast('Chart Deleted Successfully'); + chartListRes.refetch(); + }, + onError: (err: unknown) => { + toast(`Received ${String(err)} while deleting chart `); + }, + } + ); - // // setImageId(res.id); - // }, - // onError: (err: any) => { - // toast(`Received ${err} while deleting chart `); - // }, - // } - // ); + const deleteResourceChartImagemutation = useMutation( + (data: { resourceChartImageId: string }) => + GraphQL( + deleteResourceChartImage, + { + [params.entityType]: params.entitySlug, + }, + data + ), + { + onSuccess: () => { + toast('ChartImage Deleted Successfully'); + chartListRes.refetch(); + }, + onError: (err: unknown) => { + toast(`Received ${String(err)} while deleting chart `); + }, + } + ); - const handleChart = (row: any) => { + const handleChart = (row: ChartTableCellContext['row']) => { if (row.original.typename === 'TypeResourceChart') { router.push( `/dashboard/${params.entityType}/${params.entitySlug}/charts/${row.original.id}?type=TypeResourceChart` @@ -263,7 +187,7 @@ const ChartsList: React.FC = () => { { accessorKey: 'name', header: 'Name of Chart', - cell: ({ row }: any) => ( + cell: ({ row }: ChartTableCellContext) => (
handleChart(row)} @@ -291,7 +215,7 @@ const ChartsList: React.FC = () => { }, { header: 'DELETE', - cell: ({ row }: any) => ( + cell: ({ row }: ChartTableCellContext) => (
= () => { ]; }; - const generateTableData = (data: any[]) => { - return data?.map((item: any) => ({ + const generateTableData = (data: ChartListEntry[]) => { + return data?.map((item) => ({ name: item.name, type: item.chartType ? toTitleCase(item.chartType.split('_').join(' ').toLowerCase()) @@ -333,7 +257,7 @@ const ChartsList: React.FC = () => { const handleSearchChange = (e: string) => { const searchTerm = e.toLowerCase(); - const filtered = chartListRes.data?.getChartData.filter((row: any) => + const filtered = chartListRes.data?.getChartData.filter((row) => row.name.toLowerCase().includes(searchTerm) ); setFilteredRows(filtered || []); diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx index d003f7bf..f326cdee 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/components/ChartsVisualize.tsx @@ -1,4 +1,3 @@ -import { UUID } from 'crypto'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useParams } from 'next/navigation'; import { renderGeoJSON } from '@/geo_json/render_geojson'; @@ -17,41 +16,62 @@ import { datasetResource, getResourceChartDetails, } from '../queries'; +import { + ChartFilters, + ChartOptions, + ChartPreview, + ResourceData, + ResourceSchema, +} from '../types'; import ChartForm from './ChartForm'; import ChartHeader from './ChartHeader'; -interface YAxisColumnItem { - fieldName: string; - label: string; - color: string; -} -interface ChartFilters { - column: string; - operator: string; - value: string; -} - -interface ChartOptions { - aggregateType: string; - regionColumn?: string; - showLegend: boolean; - timeColumn?: string; - valueColumn?: string; - xAxisColumn: string; - xAxisLabel: string; - yAxisColumn: YAxisColumnItem[]; - yAxisLabel: string; -} - interface ChartData { chartId: string; description: string; - filters: any[]; + filters: ChartFilters[]; name: string; options: ChartOptions; resource: string; type: ChartTypes; - chart: any; + chart: ChartPreview; +} + +type ChartChangeValue = + | string + | boolean + | ChartTypes + | ChartOptions + | ChartFilters[] + | ChartData; + +interface ResourceChartDetails { + id: string; + description?: string | null; + name?: string | null; + chartType: string; + chartFilters?: Array<{ + column?: { id: string } | null; + operator: string; + value: string; + }>; + chartOptions?: { + aggregateType?: string | null; + regionColumn?: { id: string } | null; + showLegend?: boolean | null; + timeColumn?: string | { id?: string } | null; + valueColumn?: { id: string } | null; + xAxisColumn?: { id: string } | null; + xAxisLabel?: string | null; + yAxisColumn?: Array<{ + field?: { id: string } | null; + label?: string | null; + color?: string | null; + }> | null; + yAxisLabel?: string | null; + } | null; + resource?: { id: string } | null; + chart?: ChartPreview | null; } interface ResourceChartInput { @@ -65,9 +85,62 @@ interface ResourceChartInput { } interface VisualizationProps { - setType: any; - setChartId: any; - chartId: any; + setType: (type: string) => void; + setChartId: (id: string) => void; + chartId: string | null; +} + +function registerChartGeoJson(chartType: string): void { + if (chartType === 'ASSAM_DISTRICT' || chartType === 'ASSAM_RC') { + const geoJson = renderGeoJSON(chartType.toLowerCase()); + if (geoJson) { + echarts.registerMap( + chartType.toLowerCase(), + geoJson as Parameters[1] + ); + } + } +} + +function mapResourceChartToChartData( + resourceChartDetails: ResourceChartDetails +): ChartData { + const chartFilters = resourceChartDetails.chartFilters ?? []; + return { + chartId: resourceChartDetails.id, + description: resourceChartDetails.description || '', + filters: + chartFilters.length > 0 + ? chartFilters.map((filter) => ({ + column: filter.column?.id ?? '', + operator: filter.operator, + value: filter.value, + })) + : [{ column: '', operator: '==', value: '' }], + name: resourceChartDetails.name || '', + options: { + aggregateType: resourceChartDetails.chartOptions?.aggregateType ?? '', + regionColumn: resourceChartDetails.chartOptions?.regionColumn?.id, + showLegend: resourceChartDetails.chartOptions?.showLegend ?? true, + timeColumn: + typeof resourceChartDetails.chartOptions?.timeColumn === 'string' + ? resourceChartDetails.chartOptions.timeColumn + : resourceChartDetails.chartOptions?.timeColumn?.id, + valueColumn: resourceChartDetails.chartOptions?.valueColumn?.id, + xAxisColumn: resourceChartDetails.chartOptions?.xAxisColumn?.id ?? '', + xAxisLabel: resourceChartDetails.chartOptions?.xAxisLabel ?? '', + yAxisColumn: + resourceChartDetails.chartOptions?.yAxisColumn?.map((col) => ({ + fieldName: col.field?.id ?? '', + label: col.label ?? '', + color: col.color ?? '', + })) ?? [], + yAxisLabel: resourceChartDetails.chartOptions?.yAxisLabel ?? '', + }, + resource: resourceChartDetails.resource?.id ?? '', + type: resourceChartDetails.chartType as ChartTypes, + chart: resourceChartDetails.chart ?? {}, + }; } const ChartsVisualize: React.FC = ({ @@ -81,19 +154,17 @@ const ChartsVisualize: React.FC = ({ id: string; }>(); - const { data: resourceData }: { data: any } = useQuery( - [`res_charts_${params.id}`], - () => - GraphQL( - datasetResource, - { - [params.entityType]: params.entitySlug, - }, - { datasetId: params.id } - ) + const { data: resourceData } = useQuery([`res_charts_${params.id}`], () => + GraphQL( + datasetResource, + { + [params.entityType]: params.entitySlug, + }, + { datasetId: params.id } + ) ); - const { data: chartDetails, refetch }: { data: any; refetch: any } = useQuery( + const { data: chartDetails, refetch } = useQuery( [`chartdata_${params.id}`], () => GraphQL( @@ -108,10 +179,7 @@ const ChartsVisualize: React.FC = ({ {} ); - const { - data: chartsList, - refetch: chartsListRefetch, - }: { data: any; isLoading: boolean; refetch: any } = useQuery( + const { data: chartsList, refetch: chartsListRefetch } = useQuery( [`chartsList_${params.id}`], () => GraphQL( @@ -125,11 +193,8 @@ const ChartsVisualize: React.FC = ({ ) ); - const resourceChart: { - mutate: any; - isLoading: any; - } = useMutation( - (data: { resource: UUID }) => + const resourceChart = useMutation( + (data: { resource: string }) => GraphQL( CreateResourceChart, { @@ -138,16 +203,19 @@ const ChartsVisualize: React.FC = ({ data ), { - onSuccess: (res: any) => { + onSuccess: (res) => { toast('Resource Chart Created Successfully'); refetch(); setIsSheetOpen(false); setType('visualize'); - setChartId(res.addResourceChart.id); + const created = res.addResourceChart; + if (created && 'id' in created) { + setChartId(created.id); + } chartsListRefetch(); }, - onError: (err: any) => { - toast(`Received ${err} while deleting chart `, { + onError: (err: unknown) => { + toast(`Received ${String(err)} while deleting chart `, { action: { label: 'undo', onClick: () => {}, @@ -189,71 +257,37 @@ const ChartsVisualize: React.FC = ({ null ); - const [resourceSchema, setResourceSchema] = useState([]); - - useEffect(() => { + const [resourceSchema, setResourceSchema] = useState([]); + const [prevChartDetails, setPrevChartDetails] = useState(chartDetails); + const [prevResourceData, setPrevResourceData] = useState(resourceData); + if (chartDetails !== prevChartDetails || resourceData !== prevResourceData) { + setPrevChartDetails(chartDetails); + setPrevResourceData(resourceData); if (chartId && chartDetails?.resourceChart) { + const updatedData = mapResourceChartToChartData( + chartDetails.resourceChart + ); + setChartData(updatedData); + setPreviousChartData(updatedData); const resource = resourceData?.datasetResources?.find( - (r: any) => r.id === chartDetails.resourceChart.resource?.id + (r) => r.id === chartDetails.resourceChart.resource?.id ); - if (resource) { setResourceSchema(resource.schema || []); } } - }, [chartId, chartDetails, resourceData]); + } useEffect(() => { if (chartId && chartDetails?.resourceChart) { refetch(); - updateChartData(chartDetails.resourceChart); + registerChartGeoJson(chartDetails.resourceChart.chartType); } }, [chartId, chartDetails, refetch]); - const updateChartData = (resourceChart: any) => { - if ( - resourceChart.chartType === 'ASSAM_DISTRICT' || - resourceChart.chartType === 'ASSAM_RC' - ) { - echarts.registerMap( - resourceChart.chartType.toLowerCase(), - renderGeoJSON(resourceChart.chartType.toLowerCase()) - ); - } - - const updatedData: ChartData = { - chartId: resourceChart.id, - description: resourceChart.description || '', - filters: - resourceChart.chartFilters?.length > 0 - ? resourceChart.chartFilters.map((filter: any) => ({ - column: filter.column?.id, - operator: filter.operator, - value: filter.value, - })) - : [{ column: '', operator: '==', value: '' }], - name: resourceChart.name || '', - options: { - aggregateType: resourceChart?.chartOptions?.aggregateType, - regionColumn: resourceChart?.chartOptions?.regionColumn?.id, - showLegend: resourceChart?.chartOptions?.showLegend ?? true, - timeColumn: resourceChart?.chartOptions?.timeColumn, - valueColumn: resourceChart?.chartOptions?.valueColumn?.id, - xAxisColumn: resourceChart?.chartOptions?.xAxisColumn?.id, - xAxisLabel: resourceChart?.chartOptions?.xAxisLabel, - yAxisColumn: resourceChart?.chartOptions?.yAxisColumn?.map( - (col: any) => ({ - fieldName: col.field.id, - label: col.label, - color: col.color, - }) - ), - yAxisLabel: resourceChart?.chartOptions?.yAxisLabel, - }, - resource: resourceChart.resource?.id, - type: resourceChart.chartType as ChartTypes, - chart: resourceChart.chart, - }; + const updateChartData = (resourceChartDetails: ResourceChartDetails) => { + registerChartGeoJson(resourceChartDetails.chartType); + const updatedData = mapResourceChartToChartData(resourceChartDetails); setChartData(updatedData); setPreviousChartData(updatedData); }; @@ -291,9 +325,9 @@ const ChartsVisualize: React.FC = ({ } }; - const handleChange = useCallback((field: string, value: any) => { + const handleChange = useCallback((field: string, value: ChartChangeValue) => { setChartData((prevData) => { - if (field === 'type') { + if (field === 'type' && typeof value === 'string') { const newType = value as ChartTypes; return { ...prevData, @@ -301,7 +335,13 @@ const ChartsVisualize: React.FC = ({ options: getDefaultOptions(newType), }; } - if (field === 'options') { + if ( + field === 'options' && + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + 'showLegend' in value + ) { return { ...prevData, options: { @@ -327,16 +367,22 @@ const ChartsVisualize: React.FC = ({ chartInput ), { - onSuccess: (res: any) => { + onSuccess: (res) => { toast('Resource chart saved'); - const newChartId = res?.editResourceChart?.id; - updateChartData(res.editResourceChart); - setChartId(newChartId); + if ( + res?.editResourceChart && + 'id' in res.editResourceChart + ) { + const savedChart = res.editResourceChart; + const newChartId = savedChart.id; + updateChartData(savedChart); + setChartId(newChartId); + } chartsListRefetch(); refetch(); }, - onError: (err: any) => { - toast(`Received ${err} during resource chart saving`, { + onError: (err: unknown) => { + toast(`Received ${String(err)} during resource chart saving`, { action: { label: 'undo', onClick: () => {}, @@ -376,7 +422,10 @@ const ChartsVisualize: React.FC = ({ onSuccess: (data) => { setChartData((prev) => ({ ...prev, - chart: data.chart, + chart: + 'chart' in data + ? ((data as { chart?: ChartPreview }).chart ?? {}) + : {}, type: currentType, // Preserve the type from before mutation options: { ...prev.options, @@ -403,7 +452,7 @@ const ChartsVisualize: React.FC = ({ const handleResourceChange = useCallback( (value: string) => { const resource = resourceData?.datasetResources.find( - (r: any) => r.id === value + (r) => r.id === value ); if (resource) { handleChange('resource', resource.id); @@ -423,9 +472,9 @@ const ChartsVisualize: React.FC = ({ isSheetOpen={isSheetOpen} setIsSheetOpen={setIsSheetOpen} resourceChart={resourceChart} - resourceData={resourceData} - chartsList={chartsList} - chartId={chartId} + resourceData={resourceData as ResourceData} + chartsList={chartsList ?? null} + chartId={chartId ?? ''} />
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts index a1fdfa97..bb5bc2cb 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/queries/index.ts @@ -132,7 +132,7 @@ export const createChart = graphql(` } `); -export const CreateResourceChart: any = graphql(` +export const CreateResourceChart = graphql(` mutation GenerateResourceChart($resource: UUID!) { addResourceChart(resource: $resource) { __typename diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts index 9f84dc8e..5580a5d8 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/charts/types.ts @@ -25,15 +25,19 @@ export interface ChartOptions { yAxisLabel: string; } +export interface ChartPreview { + options?: Record; +} + export interface ChartData { chartId: string; description: string; - filters: any[]; + filters: ChartFilters[]; name: string; options: ChartOptions; resource: string; type: ChartTypes; - chart: any; + chart: ChartPreview; } export interface ResourceChartInput { diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx index 6b5a7784..5908b657 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/assign/page.tsx @@ -11,8 +11,22 @@ import { GraphQL } from '@/lib/api'; import { formatDate } from '@/lib/utils'; import { Loading } from '@/components/loading'; +interface SearchDataset { + id: string; + title: string; + modified?: string; + sectors?: Array<{ name?: string }>; +} + +interface AssignTableRow { + id: string; + title: string; + category: string | { name?: string } | undefined; + modified: string; +} + // prettier-ignore -const FetchCollaborativeDetails: any = graphql(` +const FetchCollaborativeDetails = graphql(` query CollaborativeDetails($filters: CollaborativeFilter) { collaboratives(filters: $filters) { id @@ -30,7 +44,7 @@ const FetchCollaborativeDetails: any = graphql(` `); // prettier-ignore -const AssignCollaborativeDatasets: any = graphql(` +const AssignCollaborativeDatasets = graphql(` mutation assignCollaborativeDatasets($collaborativeId: String!, $datasetIds: [UUID!]!) { updateCollaborativeDatasets(collaborativeId: $collaborativeId, datasetIds: $datasetIds) { ... on TypeCollaborative { @@ -53,10 +67,10 @@ const Assign = () => { const queryClient = useQueryClient(); const COLLAB_ASSIGN_TOAST_ID = 'collaboratives-assign-toast'; - const [data, setData] = useState([]); // Ensure `data` is an array - const [selectedRow, setSelectedRows] = useState([]); + const [data, setData] = useState([]); + const [selectedRow, setSelectedRows] = useState([]); - const CollaborativeDetails: { data: any; isLoading: boolean; refetch: any } = + const CollaborativeDetails = useQuery( [`Collaborative_Details`, params.id], () => @@ -77,19 +91,26 @@ const Assign = () => { } ); - const formattedData = (data: any) => - data.map((item: any) => { + const formattedData = ( + datasets: Array<{ + id: string; + title?: string | null; + modified?: string | null; + sectors?: Array<{ name?: string | null } | null> | null; + }> + ) => + datasets.map((item) => { return { title: item.title, id: item.id, - category: item.sectors[0]?.name || 'N/A', // Safeguard in case of missing category - modified: formatDate(item.modified) || '', + category: item.sectors?.[0]?.name || 'N/A', // Safeguard in case of missing category + modified: formatDate(item.modified ?? null) || '', }; }); useEffect(() => { fetchDatasets('?size=1000&page=1') - .then((res) => { + .then((res: { results: SearchDataset[] }) => { setData(res.results); }) .catch((err) => { @@ -103,13 +124,13 @@ const Assign = () => { { accessorKey: 'modified', header: 'Last Modified' }, ]; - const generateTableData = (list: Array) => { + const generateTableData = (list: SearchDataset[]) => { return list.map((item) => { return { title: item.title, id: item.id, - category: item.sectors[0], - modified: formatDate(item.modified) || '', + category: item.sectors?.[0], + modified: formatDate(item.modified ?? null) || '', }; }); }; @@ -124,7 +145,7 @@ const Assign = () => { { collaborativeId: params.id, datasetIds: Array.isArray(selectedRow) - ? selectedRow.map((row: any) => row.id) + ? selectedRow.map((row) => row.id) : [], } ), @@ -146,7 +167,7 @@ const Assign = () => { `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/usecases` ); }, - onError: (err: any) => { + onError: (err: unknown) => { toast(`Received ${err} on dataset publish `, { id: COLLAB_ASSIGN_TOAST_ID, }); @@ -156,7 +177,7 @@ const Assign = () => { return ( <> - {CollaborativeDetails?.data?.collaboratives[0]?.datasets?.length >= 0 && + {((CollaborativeDetails?.data?.collaboratives?.[0]?.datasets?.length ?? -1) >= 0) && data.length > 0 && !CollaborativeDetails.isLoading ? ( <> @@ -177,7 +198,7 @@ const Assign = () => { columns={columns} rows={generateTableData(data)} defaultSelectedRows={formattedData( - CollaborativeDetails?.data?.collaboratives[0]?.datasets + CollaborativeDetails?.data?.collaboratives?.[0]?.datasets ?? [] )} onRowSelectionChange={(selected) => { setSelectedRows(Array.isArray(selected) ? selected : []); // Ensure selected is always an array diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx index d58be3dd..030c78c3 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/EntitySelection.tsx @@ -4,9 +4,17 @@ import { Button, Icon, Text } from 'opub-ui'; import { Icons } from '@/components/icons'; import CustomCombobox from '../../../../usecases/edit/[id]/contributors/CustomCombobox'; -type Option = { label: string; value: string }; +interface Option { + label: string; + value: string; +} + +interface EntityWithLogo { + id: string; + logo?: { url?: string | null } | null; +} -type EntitySectionProps = { +interface EntitySectionProps { title: string; label: string; placeholder: string; @@ -14,8 +22,8 @@ type EntitySectionProps = { selectedValues: Option[]; onChange: (values: Option[]) => void; onRemove: (value: Option) => void; - data: any; -}; + data?: EntityWithLogo[] | null; +} const EntitySection = ({ title, @@ -54,9 +62,9 @@ const EntitySection = ({
org.id === item.value)?.logo?.url + data?.find((org) => org.id === item.value)?.logo?.url ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${ - data?.find((org: any) => org.id === item.value)?.logo + data?.find((org) => org.id === item.value)?.logo ?.url }` : '/org.png' diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx index cfaf4b59..2bfe9645 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/page.tsx @@ -40,7 +40,7 @@ const Details = () => { const COLLAB_CONTRIBUTORS_TOAST_ID = 'collaboratives-contributors-toast'; - const Users: { data: any; isLoading: boolean; refetch: any } = useQuery( + const Users = useQuery( [`fetch_users`], () => GraphQL( @@ -59,18 +59,16 @@ const Details = () => { } ); - const Organizations: { data: any; isLoading: boolean; refetch: any } = + const Organizations = useQuery([`fetch_orgs`], () => GraphQL( OrgList, { [params.entityType]: params.entitySlug, - }, - [] - ) + }) ); - const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = + const CollaborativeData = useQuery( [`fetch_collaborative_${params.id}`], () => @@ -91,32 +89,36 @@ const Details = () => { } ); - useEffect(() => { + const [prevCollabData, setPrevCollabData] = useState< + typeof CollaborativeData.data | undefined + >(undefined); + if (CollaborativeData.data !== prevCollabData) { + setPrevCollabData(CollaborativeData.data); setFormData((prev) => ({ ...prev, partners: CollaborativeData?.data?.collaboratives?.[0]?.partnerOrganizations?.map( - (org: any) => ({ + (org) => ({ label: org.name, value: org.id, }) ) || [], supporters: CollaborativeData?.data?.collaboratives?.[0]?.supportingOrganizations?.map( - (org: any) => ({ + (org) => ({ label: org.name, value: org.id, }) ) || [], contributors: CollaborativeData?.data?.collaboratives?.[0]?.contributors?.map( - (user: any) => ({ + (user) => ({ label: user.fullName, value: user.id, }) ) || [], })); - }, [CollaborativeData?.data]); + } const { mutate: addContributor, isLoading: addContributorLoading } = useMutation( @@ -145,8 +147,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID, }); }, @@ -180,8 +182,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID, }); }, @@ -214,8 +216,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID }); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID }); }, } ); @@ -247,8 +249,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID, }); }, @@ -281,8 +283,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID }); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID }); }, } ); @@ -314,8 +316,8 @@ const Details = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_CONTRIBUTORS_TOAST_ID, }); }, @@ -329,7 +331,7 @@ const Details = () => { const selectedContributors = formData.contributors; const options = - Users?.data?.searchUsers?.map((user: any) => ({ + Users?.data?.searchUsers?.map((user) => ({ label: user.fullName, value: user.id, })) || []; @@ -375,12 +377,12 @@ const Details = () => { { + onChange={(newValues) => { const prevValues = formData.contributors.map( (item) => item.value ); const newlyAdded = newValues.find( - (item: any) => !prevValues.includes(item.value) + (item) => !prevValues.includes(item.value) ); setFormData((prev) => ({ @@ -397,7 +399,7 @@ const Details = () => { setSearchValue(''); // clear input }} placeholder="Add Contributors" - onInput={(value: any) => { + onInput={(value: string) => { setSearchValue(value); }} /> @@ -410,12 +412,12 @@ const Details = () => { > contributor.id === item.value + CollaborativeData.data?.collaboratives?.[0]?.contributors?.find( + (contributor) => contributor.id === item.value )?.profilePicture?.url ? `${process.env.NEXT_PUBLIC_BACKEND_URL}/${ - CollaborativeData.data.collaboratives[0]?.contributors?.find( - (contributor: any) => + CollaborativeData.data?.collaboratives?.[0]?.contributors?.find( + (contributor) => contributor.id === item.value )?.profilePicture?.url }` @@ -462,16 +464,16 @@ const Details = () => { ?.supportingOrganizations } options={(Organizations?.data?.allOrganizations || [])?.map( - (org: any) => ({ + (org) => ({ label: org.name, value: org.id, }) )} selectedValues={formData.supporters} - onChange={(newValues: any) => { + onChange={(newValues) => { const prevValues = formData.supporters.map((item) => item.value); const newlyAdded = newValues.find( - (item: any) => !prevValues.includes(item.value) + (item) => !prevValues.includes(item.value) ); setFormData((prev) => ({ ...prev, supporters: newValues })); @@ -483,7 +485,7 @@ const Details = () => { }); } }} - onRemove={(item: any) => { + onRemove={(item) => { setFormData((prev) => ({ ...prev, supporters: prev.supporters.filter( @@ -505,16 +507,16 @@ const Details = () => { CollaborativeData?.data?.collaboratives[0]?.partnerOrganizations } options={(Organizations?.data?.allOrganizations || [])?.map( - (org: any) => ({ + (org) => ({ label: org.name, value: org.id, }) )} selectedValues={formData.partners} - onChange={(newValues: any) => { + onChange={(newValues) => { const prevValues = formData.partners.map((item) => item.value); const newlyAdded = newValues.find( - (item: any) => !prevValues.includes(item.value) + (item) => !prevValues.includes(item.value) ); setFormData((prev) => ({ ...prev, partners: newValues })); @@ -526,7 +528,7 @@ const Details = () => { }); } }} - onRemove={(item: any) => { + onRemove={(item) => { setFormData((prev) => ({ ...prev, partners: prev.partners.filter((s) => s.value !== item.value), diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts index ab68111f..6b0ffefe 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/contributors/query.ts @@ -1,7 +1,7 @@ import { graphql } from '@/gql'; -export const FetchUsers: any = graphql(` +export const FetchUsers = graphql(` query searchUsers($limit: Int!, $searchTerm: String!) { searchUsers(limit: $limit, searchTerm: $searchTerm) { id @@ -11,7 +11,7 @@ export const FetchUsers: any = graphql(` } `); -export const FetchCollaborativeInfo: any = graphql(` +export const FetchCollaborativeInfo = graphql(` query collaborativeinfo($filters: CollaborativeFilter) { collaboratives(filters: $filters) { id @@ -44,7 +44,7 @@ export const FetchCollaborativeInfo: any = graphql(` } `); -export const AddContributors: any = graphql(` +export const AddContributors = graphql(` mutation addContributorToCollaborative($collaborativeId: String!, $userId: ID!) { addContributorToCollaborative(collaborativeId: $collaborativeId, userId: $userId) { __typename @@ -61,7 +61,7 @@ export const AddContributors: any = graphql(` } `); -export const RemoveContributor: any = graphql(` +export const RemoveContributor = graphql(` mutation removeContributorFromCollaborative($collaborativeId: String!, $userId: ID!) { removeContributorFromCollaborative(collaborativeId: $collaborativeId, userId: $userId) { __typename @@ -78,7 +78,7 @@ export const RemoveContributor: any = graphql(` } `); -export const AddSupporters: any = graphql(` +export const AddSupporters = graphql(` mutation addSupportingOrganizationToCollaborative( $collaborativeId: String! $organizationId: ID! @@ -102,7 +102,7 @@ export const AddSupporters: any = graphql(` } `); -export const RemoveSupporters: any = graphql(` +export const RemoveSupporters = graphql(` mutation removeSupportingOrganizationFromCollaborative( $collaborativeId: String! $organizationId: ID! @@ -126,7 +126,7 @@ export const RemoveSupporters: any = graphql(` } `); -export const AddPartners: any = graphql(` +export const AddPartners = graphql(` mutation addPartnerOrganizationToCollaborative( $collaborativeId: String! $organizationId: ID! @@ -150,7 +150,7 @@ export const AddPartners: any = graphql(` } `); -export const RemovePartners: any = graphql(` +export const RemovePartners = graphql(` mutation removePartnerOrganizationFromCollaborative( $collaborativeId: String! $organizationId: ID! @@ -175,7 +175,7 @@ export const RemovePartners: any = graphql(` `); -export const OrgList: any = graphql(` +export const OrgList = graphql(` query allOrgs { allOrganizations { id diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx index b463fcfb..fd98baf0 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/details/page.tsx @@ -12,8 +12,26 @@ import { RichTextEditor } from '@/components/RichTextEditor'; import { useEditStatus } from '../../context'; import Metadata from '../metadata/page'; +interface UploadedImage { + name?: string | null; + path?: string | null; + url?: string | null; +} + +interface CollaborativeFormData { + title: string; + summary: string; + logo: File | UploadedImage | null; + coverImage: File | UploadedImage | null; + slug: string; + status: string; + startedOn: string | null; + completedOn: string | null; + platformUrl: string; +} + // prettier-ignore -const UpdateCollaborativeMutation: any = graphql(` +const UpdateCollaborativeMutation = graphql(` mutation updateCollaborative($data: CollaborativeInputPartial!) { updateCollaborative(data: $data) { __typename @@ -42,7 +60,7 @@ const UpdateCollaborativeMutation: any = graphql(` `); //prettier-ignore -const FetchCollaborative: any = graphql(` +const FetchCollaborative = graphql(` query CollaborativeData($filters: CollaborativeFilter) { collaboratives(filters: $filters) { id @@ -77,7 +95,7 @@ const Details = () => { const queryClient = useQueryClient(); const COLLAB_DETAILS_TOAST_ID = 'collaboratives-details-toast'; - const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = + const CollaborativeData = useQuery( [ `fetch_CollaborativeData_details`, @@ -113,17 +131,19 @@ const Details = () => { const initialFormData = { title: '', summary: '', - logo: null as File | null, - coverImage: null as File | null, + logo: null as File | UploadedImage | null, + coverImage: null as File | UploadedImage | null, slug: '', status: '', - startedOn: null, - completedOn: null, + startedOn: null as string | null, + completedOn: null as string | null, platformUrl: '', }; - const [formData, setFormData] = useState(initialFormData); - const [previousFormData, setPreviousFormData] = useState(initialFormData); + const [formData, setFormData] = + useState(initialFormData); + const [previousFormData, setPreviousFormData] = + useState(initialFormData); const [slugError, setSlugError] = useState(null); const validateSlug = (value: string) => { @@ -138,7 +158,16 @@ const Details = () => { return null; }; - useEffect(() => { + const [prevCollaborativesData, setPrevCollaborativesData] = useState< + typeof CollaborativesData | undefined + >(undefined); + const [prevDetailsId, setPrevDetailsId] = useState(params.id); + if ( + params.id !== prevDetailsId || + CollaborativesData !== prevCollaborativesData + ) { + setPrevDetailsId(params.id); + setPrevCollaborativesData(CollaborativesData); if (CollaborativesData) { const updatedData = { title: CollaborativesData.title || '', @@ -154,7 +183,7 @@ const Details = () => { setFormData(updatedData); setPreviousFormData(updatedData); } - }, [params.id, CollaborativesData]); + } const { mutate, isLoading: editMutationLoading } = useMutation( (data: { data: CollaborativeInputPartial }) => @@ -166,17 +195,33 @@ const Details = () => { data ), { - onSuccess: (res: any) => { + onSuccess: (res) => { toast('Collaborative updated successfully', { id: COLLAB_DETAILS_TOAST_ID, }); setFormData((prev) => ({ ...prev, - ...res.updateCollaborative, + title: res.updateCollaborative.title ?? prev.title, + summary: res.updateCollaborative.summary ?? prev.summary, + logo: res.updateCollaborative.logo ?? prev.logo, + coverImage: res.updateCollaborative.coverImage ?? prev.coverImage, + slug: res.updateCollaborative.slug ?? prev.slug, + status: res.updateCollaborative.status ?? prev.status, + startedOn: res.updateCollaborative.startedOn ?? prev.startedOn, + completedOn: res.updateCollaborative.completedOn ?? prev.completedOn, + platformUrl: res.updateCollaborative.platformUrl ?? prev.platformUrl, })); setPreviousFormData((prev) => ({ ...prev, - ...res.updateCollaborative, + title: res.updateCollaborative.title ?? prev.title, + summary: res.updateCollaborative.summary ?? prev.summary, + logo: res.updateCollaborative.logo ?? prev.logo, + coverImage: res.updateCollaborative.coverImage ?? prev.coverImage, + slug: res.updateCollaborative.slug ?? prev.slug, + status: res.updateCollaborative.status ?? prev.status, + startedOn: res.updateCollaborative.startedOn ?? prev.startedOn, + completedOn: res.updateCollaborative.completedOn ?? prev.completedOn, + platformUrl: res.updateCollaborative.platformUrl ?? prev.platformUrl, })); queryClient.invalidateQueries({ @@ -188,22 +233,36 @@ const Details = () => { ], }); }, - onError: (error: any) => { - const msg = - error?.response?.errors?.[0]?.message ?? - error?.message ?? - 'Something went wrong'; + onError: (error: unknown) => { + let msg = 'Something went wrong'; + if (typeof error === 'object' && error !== null) { + if ( + 'response' in error && + typeof error.response === 'object' && + error.response !== null && + 'errors' in error.response && + Array.isArray(error.response.errors) && + typeof error.response.errors[0]?.message === 'string' + ) { + msg = error.response.errors[0].message; + } else if ('message' in error && typeof error.message === 'string') { + msg = error.message; + } + } toast(`Error: ${msg}`, { id: COLLAB_DETAILS_TOAST_ID }); }, } ); - const handleChange = useCallback((field: string, value: any) => { + const handleChange = useCallback( + (field: keyof CollaborativeFormData, value: CollaborativeFormData[keyof CollaborativeFormData]) => { setFormData((prevData) => ({ ...prevData, [field]: value, })); - }, []); + }, + [] + ); const onDrop = React.useCallback( (_dropFiles: File[], acceptedFiles: File[]) => { @@ -229,7 +288,7 @@ const Details = () => { [mutate, params.id] ); - const handleSave = (updatedData: any) => { + const handleSave = (updatedData: CollaborativeFormData) => { const slugErr = validateSlug(updatedData?.slug || ''); if (slugErr) { return; @@ -243,8 +302,8 @@ const Details = () => { id: params.id.toString(), title: updatedData.title, summary: updatedData.summary, - startedOn: (updatedData.startedOn as Date) || null, - completedOn: (updatedData.completedOn as Date) || null, + startedOn: updatedData.startedOn || null, + completedOn: updatedData.completedOn || null, platformUrl: updatedData.platformUrl || '', slug: updatedData.slug || '', }, @@ -379,7 +438,7 @@ const Details = () => { formData.logo && typeof formData.logo === 'object' && 'name' in formData.logo - ? (formData.logo as any).name?.split('/').pop() || 'Logo file' + ? formData.logo.name?.split('/').pop() || 'Logo file' : 'Name of the logo' } /> @@ -401,7 +460,7 @@ const Details = () => { formData.coverImage && typeof formData.coverImage === 'object' && 'name' in formData.coverImage - ? (formData.coverImage as any).name?.split('/').pop() || + ? formData.coverImage.name?.split('/').pop() || 'Cover image file' : 'Name of the cover image' } diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx index 9114de06..4da96aa3 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/metadata/page.tsx @@ -3,15 +3,83 @@ import { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; -import { MetadataModels } from '@/gql/generated/graphql'; +import { + MetadataModels, + UpdateCollaborativeMetadataInput, +} from '@/gql/generated/graphql'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Combobox, Spinner, toast } from 'opub-ui'; import { GraphQL } from '@/lib/api'; import { useEditStatus } from '../../context'; +interface SelectOption { + label: string; + value: string; +} + +interface MetadataField { + id: string; + label: string; + dataType: string; + options?: string[] | null; +} + +type MetadataFormValue = string | SelectOption[] | SelectOption | undefined; + +interface MetadataFormData { + [key: string]: MetadataFormValue; +} + +interface MetadataSource { + metadata?: Array<{ + value?: string | null; + metadataItem: { id: string; dataType: string }; + }> | null; + sectors?: Array<{ id: string; name?: string | null }> | null; + sdgs?: Array<{ + id: string; + name?: string | null; + code: string; + number?: number | null; + }> | null; + tags?: Array<{ id: string; value?: string | null }> | null; + geographies?: Array<{ id: string; name?: string | null }> | null; +} + +function comboValues(value: MetadataFormValue, key: 'value' | 'label'): string[] { + if (!Array.isArray(value)) return []; + return value.map((item) => { + if (typeof item === 'object' && item !== null) { + return String(item[key] ?? ''); + } + return String(item); + }); +} + +function asSelectOptions(value: MetadataFormValue): SelectOption[] { + return Array.isArray(value) ? value : []; +} + +function comboboxSelected( + value: MetadataFormValue +): string | SelectOption[] | undefined { + if (value === undefined) return undefined; + if (typeof value === 'string') return value; + if (Array.isArray(value)) return value; + return [value]; +} + +function metadataValue(value: MetadataFormValue): string { + if (Array.isArray(value)) { + return value.map((item) => item.value || String(item)).join(', '); + } + if (typeof value === 'string') return value; + return value?.value ?? ''; +} + // prettier-ignore -const FetchCollaborativeMetadata: any = graphql(` +const FetchCollaborativeMetadata = graphql(` query CollaborativeMetadata($filters: CollaborativeFilter) { collaboratives(filters: $filters) { id @@ -64,7 +132,7 @@ const metadataQueryDoc = graphql(` `); // prettier-ignore -const sectorsListQueryDoc: any = graphql(` +const sectorsListQueryDoc = graphql(` query SectorList { sectors { id @@ -74,7 +142,7 @@ const sectorsListQueryDoc: any = graphql(` `); // prettier-ignore -const sdgsListQueryDoc: any = graphql(` +const sdgsListQueryDoc = graphql(` query SDGList { sdgs { id @@ -86,7 +154,7 @@ const sdgsListQueryDoc: any = graphql(` `); // prettier-ignore -const tagsListQueryDoc: any = graphql(` +const tagsListQueryDoc = graphql(` query TagsList { tags { id @@ -96,7 +164,7 @@ const tagsListQueryDoc: any = graphql(` `); // prettier-ignore -const geographiesListQueryDoc: any = graphql(` +const geographiesListQueryDoc = graphql(` query GeographiesList { geographies { id @@ -112,7 +180,7 @@ const geographiesListQueryDoc: any = graphql(` `); // prettier-ignore -const UpdateCollaborativeMetadata: any = graphql(` +const UpdateCollaborativeMetadata = graphql(` mutation addUpdateCollaborativeMetadata($updateMetadataInput: UpdateCollaborativeMetadataInput!) { addUpdateCollaborativeMetadata(updateMetadataInput: $updateMetadataInput) { __typename @@ -162,7 +230,7 @@ const Metadata = () => { const { setStatus } = useEditStatus(); const queryClient = useQueryClient(); - const collaborativeData: { data: any; isLoading: boolean } = useQuery( + const collaborativeData = useQuery( [ `fetch_CollaborativeData_Metadata`, params.entityType, @@ -200,13 +268,11 @@ const Metadata = () => { ) ); - const defaultValuesPrepFn = (data: any) => { - let defaultVal: { - [key: string]: any; - } = {}; + const defaultValuesPrepFn = (data: MetadataSource): MetadataFormData => { + const defaultVal: MetadataFormData = {}; - data?.metadata?.map((field: any) => { - if (field.metadataItem.dataType === 'MULTISELECT' && field.value !== '') { + data?.metadata?.map((field) => { + if (field.metadataItem.dataType === 'MULTISELECT' && field.value) { defaultVal[field.metadataItem.id] = field.value .split(', ') .map((value: string) => ({ @@ -221,36 +287,36 @@ const Metadata = () => { }); defaultVal['sectors'] = - data?.sectors?.map((sector: any) => { + data?.sectors?.map((sector) => { return { - label: sector.name, + label: sector.name ?? '', value: sector.id, }; }) || []; defaultVal['sdgs'] = - data?.sdgs?.map((sdg: any) => { + data?.sdgs?.map((sdg) => { const num = sdg.number ? String(sdg.number).padStart(2, '0') : sdg.code.replace('SDG', '').padStart(2, '0'); return { - label: `${num}. ${sdg.name}`, + label: `${num}. ${sdg.name ?? ''}`, value: sdg.id, }; }) || []; defaultVal['tags'] = - data?.tags?.map((tag: any) => { + data?.tags?.map((tag) => { return { - label: tag.value, + label: tag.value ?? '', value: tag.id, }; }) || []; defaultVal['geographies'] = - data?.geographies?.map((geo: any) => { + data?.geographies?.map((geo) => { return { - label: geo.name, + label: geo.name ?? '', value: geo.id, }; }) || []; @@ -262,8 +328,11 @@ const Metadata = () => { defaultValuesPrepFn(collaborativeData?.data?.collaboratives?.[0] || {}) ); const [previousFormData, setPreviousFormData] = useState(formData); - - useEffect(() => { + const [prevCollaborativeData, setPrevCollaborativeData] = useState( + collaborativeData.data + ); + if (collaborativeData.data !== prevCollaborativeData) { + setPrevCollaborativeData(collaborativeData.data); if (collaborativeData.data?.collaboratives?.[0]) { const updatedData = defaultValuesPrepFn( collaborativeData.data.collaboratives[0] @@ -271,55 +340,42 @@ const Metadata = () => { setFormData(updatedData); setPreviousFormData(updatedData); } - }, [collaborativeData.data]); + } - const getSectorsList: { data: any; isLoading: boolean; error: any } = + const getSectorsList = useQuery([`sectors_list_query`], () => GraphQL( sectorsListQueryDoc, { [params.entityType]: params.entitySlug, - }, - [] - ) + }) ); - const getSDGsList: { data: any; isLoading: boolean; error: any } = useQuery( + const getSDGsList = useQuery( [`sdgs_list_query`], () => GraphQL( sdgsListQueryDoc, { [params.entityType]: params.entitySlug, - }, - [] - ) + }) ); - const getTagsList: { - data: any; - isLoading: boolean; - error: any; - refetch: any; - } = useQuery([`tags_list_query`], () => + const getTagsList = useQuery([`tags_list_query`], () => GraphQL( tagsListQueryDoc, { [params.entityType]: params.entitySlug, - }, - [] - ) + }) ); - const getGeographiesList: { data: any; isLoading: boolean; error: any } = + const getGeographiesList = useQuery([`geographies_list_query`], () => GraphQL( geographiesListQueryDoc, { [params.entityType]: params.entitySlug, - }, - [] - ) + }) ); const [isTagsListUpdated, setIsTagsListUpdated] = useState(false); @@ -327,7 +383,7 @@ const Metadata = () => { // Update mutation const updateCollaborative = useMutation( - (data: { updateMetadataInput: any }) => + (data: { updateMetadataInput: UpdateCollaborativeMetadataInput }) => GraphQL( UpdateCollaborativeMetadata, { @@ -336,19 +392,20 @@ const Metadata = () => { data ), { - onSuccess: (res: any) => { + onSuccess: (res) => { toast('Collaborative updated successfully', { id: COLLAB_METADATA_TOAST_ID, }); - const updatedData = defaultValuesPrepFn( - res.addUpdateCollaborativeMetadata - ); - if (isTagsListUpdated) { - getTagsList.refetch(); - setIsTagsListUpdated(false); + const result = res.addUpdateCollaborativeMetadata; + if (result.__typename === 'TypeCollaborative') { + const updatedData = defaultValuesPrepFn(result); + if (isTagsListUpdated) { + getTagsList.refetch(); + setIsTagsListUpdated(false); + } + setFormData(updatedData); + setPreviousFormData(updatedData); } - setFormData(updatedData); - setPreviousFormData(updatedData); // Keep other edit tabs in sync (Details/Publish) without requiring a full reload. queryClient.invalidateQueries({ @@ -376,20 +433,20 @@ const Metadata = () => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { id: COLLAB_METADATA_TOAST_ID }); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_METADATA_TOAST_ID }); }, } ); - const handleChange = (field: string, value: any) => { - setFormData((prevData: any) => ({ + const handleChange = (field: string, value: MetadataFormValue) => { + setFormData((prevData) => ({ ...prevData, [field]: value, })); }; - const handleSave = (updatedData: any) => { + const handleSave = (updatedData: MetadataFormData) => { if (JSON.stringify(updatedData) !== JSON.stringify(previousFormData)) { setPreviousFormData(updatedData); @@ -400,23 +457,18 @@ const Metadata = () => { .filter( (key) => !['tags', 'sectors', 'sdgs'].includes(key) && - metadataFields?.metadata?.find((item: any) => item.id === key) + metadataFields?.metadata?.find((item) => item.id === key) ) .map((key) => ({ id: key, - value: Array.isArray(updatedData[key]) - ? updatedData[key] - .map((item: any) => item.value || item) - .join(', ') - : updatedData[key], + value: metadataValue(updatedData[key]), })), - sectors: updatedData.sectors?.map((item: any) => item.value) || [], - sdgs: updatedData.sdgs?.map((item: any) => item.value) || [], - tags: updatedData.tags?.map((item: any) => item.label) || [], - geographies: - updatedData.geographies?.map((item: any) => - parseInt(item.value, 10) - ) || [], + sectors: comboValues(updatedData.sectors, 'value'), + sdgs: comboValues(updatedData.sdgs, 'value'), + tags: comboValues(updatedData.tags, 'label'), + geographies: comboValues(updatedData.geographies, 'value').map((value) => + parseInt(value, 10) + ), }, }); } @@ -440,18 +492,20 @@ const Metadata = () => { ); } - function renderInputField(metadataFormItem: any) { + function renderInputField(metadataFormItem: MetadataField) { if (metadataFormItem.dataType === 'SELECT') { return (
({ - label: option, - value: option, - }))} + list={ + metadataFormItem.options?.map((option: string) => ({ + label: option, + value: option, + })) || [] + } label={metadataFormItem.label} - selectedValue={formData[metadataFormItem.id]} + selectedValue={comboboxSelected(formData[metadataFormItem.id])} displaySelected onChange={(value) => { handleChange(metadataFormItem.id, value); @@ -468,13 +522,13 @@ const Metadata = () => { ({ + ...(metadataFormItem.options?.map((option: string) => ({ label: option, value: option, })) || []), ]} label={metadataFormItem.label + ' *'} - selectedValue={formData[metadataFormItem.id]} + selectedValue={comboboxSelected(formData[metadataFormItem.id])} displaySelected onChange={(value) => { handleChange(metadataFormItem.id, value); @@ -496,7 +550,7 @@ const Metadata = () => { label="SDG Goals *" name="sdgs" list={ - getSDGsList?.data?.sdgs?.map((item: any) => { + getSDGsList?.data?.sdgs?.map((item) => { const num = item.number ? String(item.number).padStart(2, '0') : item.code.replace('SDG', '').padStart(2, '0'); @@ -506,7 +560,7 @@ const Metadata = () => { }; }) || [] } - selectedValue={formData.sdgs} + selectedValue={asSelectOptions(formData.sdgs)} onChange={(value) => { handleChange('sdgs', value); handleSave({ ...formData, sdgs: value }); @@ -520,13 +574,13 @@ const Metadata = () => { label="Tags" creatable list={ - getTagsList?.data.tags?.map((item: any) => ({ - label: item.value, + getTagsList?.data?.tags?.map((item) => ({ + label: item.value ?? '', value: item.id, })) || [] } key={`tags-${getTagsList.data?.tags?.length}`} - selectedValue={formData.tags} + selectedValue={asSelectOptions(formData.tags)} onChange={(value) => { setIsTagsListUpdated(true); handleChange('tags', value); @@ -540,12 +594,12 @@ const Metadata = () => { label="Sectors *" name="sectors" list={ - getSectorsList?.data.sectors?.map((item: any) => ({ + getSectorsList?.data?.sectors?.map((item) => ({ label: item.name, value: item.id, })) || [] } - selectedValue={formData.sectors} + selectedValue={asSelectOptions(formData.sectors)} onChange={(value) => { handleChange('sectors', value); handleSave({ ...formData, sectors: value }); @@ -558,12 +612,12 @@ const Metadata = () => { label="Geographies" name="geographies" list={ - getGeographiesList?.data.geographies?.map((item: any) => ({ + getGeographiesList?.data?.geographies?.map((item) => ({ label: `${item.name}${item.parentId ? ` (${item.parentId.name})` : ''}`, value: item.id, })) || [] } - selectedValue={formData.geographies} + selectedValue={asSelectOptions(formData.geographies)} onChange={(value) => { handleChange('geographies', value); handleSave({ ...formData, geographies: value }); @@ -573,7 +627,7 @@ const Metadata = () => {
- {metadataFields?.metadata?.map((item: any) => renderInputField(item))} + {metadataFields?.metadata?.map((item) => renderInputField(item))}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx index 2dd8ded8..b91b1a81 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Assign.tsx @@ -2,20 +2,42 @@ import { Table } from 'opub-ui'; import { formatDate } from '@/lib/utils'; -const Assign = ({ data }: { data: any }) => { +interface AssignItem { + id: string; + title?: string | null; + modified?: string | null; + sectors?: Array<{ name?: string | null } | null> | null; +} + +interface AssignProps { + data?: unknown; +} + +function isAssignItem(value: unknown): value is AssignItem { + return ( + typeof value === 'object' && + value !== null && + 'id' in value && + 'title' in value + ); +} + +const Assign = ({ data }: AssignProps) => { const columns = [ { accessorKey: 'title', header: 'Title' }, { accessorKey: 'sector', header: 'Sector' }, { accessorKey: 'modified', header: 'Last Modified' }, ]; - const generateTableData = (list: Array) => { - return list?.map((item) => { + const list = Array.isArray(data) ? data.filter(isAssignItem) : []; + + const generateTableData = (items: AssignItem[]) => { + return items.map((item) => { return { title: item.title, id: item.id, - sector: item.sectors[0]?.name, - modified: formatDate(item.modified) || '', + sector: item.sectors?.[0]?.name, + modified: formatDate(item.modified ?? null) || '', }; }); }; @@ -23,8 +45,8 @@ const Assign = ({ data }: { data: any }) => {
); diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx index e215c296..2db59be3 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Contributors.tsx @@ -1,51 +1,85 @@ import Image from 'next/image'; import { Text } from 'opub-ui'; -const Contributors = ({ data }: { data: any }) => { - const ContributorDetails = [ +interface Contributor { + fullName?: string | null; + profilePicture?: { url?: string | null } | null; +} + +interface Organization { + name?: string | null; + logo?: { url?: string | null } | null; +} + +interface CollaborativeContributorsData { + collaboratives: Array<{ + contributors?: Contributor[] | null; + supportingOrganizations?: Organization[] | null; + partnerOrganizations?: Organization[] | null; + } | null> | null; +} + +interface ContributorsProps { + data?: CollaborativeContributorsData | null; +} + +interface ContributorSection { + label: string; + value: string; + image: Contributor[]; +} + +interface OrgSection { + label: string; + value: string; + image: Organization[]; +} + +const Contributors = ({ data }: ContributorsProps) => { + const ContributorDetails: ContributorSection[] = [ { label: 'Contributors', value: - data?.collaboratives[0]?.contributors.length > 0 - ? data?.collaboratives[0]?.contributors - .map((item: any) => item.fullName) - .join(', ') + (data?.collaboratives?.[0]?.contributors?.length ?? 0) > 0 + ? data?.collaboratives?.[0]?.contributors + ?.map((item) => item.fullName) + .join(', ') || 'No Contributors' : 'No Contributors', - image: data?.collaboratives[0]?.contributors, + image: data?.collaboratives?.[0]?.contributors ?? [], }, ]; - const OrgDetails = [ + const OrgDetails: OrgSection[] = [ { label: 'Supporters', value: - data?.collaboratives[0]?.supportingOrganizations.length > 0 - ? data?.collaboratives[0]?.supportingOrganizations - .map((item: any) => item.name) - .join(', ') + (data?.collaboratives?.[0]?.supportingOrganizations?.length ?? 0) > 0 + ? data?.collaboratives?.[0]?.supportingOrganizations + ?.map((item) => item.name) + .join(', ') || 'No Supporting Organizations' : 'No Supporting Organizations', - image: data?.collaboratives[0]?.supportingOrganizations, + image: data?.collaboratives?.[0]?.supportingOrganizations ?? [], }, { label: 'Partners', value: - data?.collaboratives[0]?.partnerOrganizations.length > 0 - ? data?.collaboratives[0]?.partnerOrganizations - .map((item: any) => item.name) - .join(', ') + (data?.collaboratives?.[0]?.partnerOrganizations?.length ?? 0) > 0 + ? data?.collaboratives?.[0]?.partnerOrganizations + ?.map((item) => item.name) + .join(', ') || 'No Partner Organizations' : 'No Partner Organizations', - image: data?.collaboratives[0]?.partnerOrganizations, + image: data?.collaboratives?.[0]?.partnerOrganizations ?? [], }, ]; return (
- {ContributorDetails.map((item: any, index: number) => ( + {ContributorDetails.map((item, index) => (
{item.label}:
- {item?.image.map((data: any, index: number) => ( + {item?.image.map((data, index) => (
{
))} - {OrgDetails.map((item: any, index: number) => ( + {OrgDetails.map((item, index) => (
{item.label}:
- {item.image.map((data: any, index: number) => ( + {item.image.map((data, index) => (
{ ); }; -export default Contributors; \ No newline at end of file +export default Contributors; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx index fa22f37c..60b06d71 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/publish/Details.tsx @@ -6,17 +6,57 @@ import { Tag, Text } from 'opub-ui'; import { getWebsiteTitle } from '@/lib/utils'; import { RichTextRenderer } from '@/components/RichTextRenderer'; -const Details = ({ data }: { data: any }) => { - const [platformTitle, setPlatformTitle] = useState(null); +interface NamedItem { + id?: string | null; + name?: string | null; + code?: string | null; + value?: string | null; +} + +interface CollaborativePublishDetails { + title?: string | null; + summary?: string | null; + runningStatus?: string | null; + startedOn?: string | null; + completedOn?: string | null; + platformUrl?: string | { value?: string } | null; + sectors?: NamedItem[] | null; + sdgs?: NamedItem[] | null; + tags?: NamedItem[] | null; + metadata?: Array<{ + value?: string | null; + metadataItem?: { label?: string | null } | null; + }> | null; + logo?: { path?: string | null } | null; + coverImage?: { path?: string | null } | null; +} + +interface DetailsProps { + data?: { collaboratives?: Array | null } | null; +} + +const Details = ({ data }: DetailsProps) => { const collaborative = data?.collaboratives?.[0]; const platformUrl = collaborative?.platformUrl; + const [platformTitle, setPlatformTitle] = useState( + platformUrl === null ? 'N/A' : null + ); + const [prevPlatformUrl, setPrevPlatformUrl] = useState(platformUrl); + if (platformUrl !== prevPlatformUrl) { + setPrevPlatformUrl(platformUrl); + if (platformUrl === null) { + setPlatformTitle('N/A'); + } + } useEffect(() => { + if (!collaborative || platformUrl === null) return; + const fetchTitle = async () => { try { const urlItem = collaborative?.platformUrl; - if (urlItem && urlItem.value) { + if (urlItem && typeof urlItem === 'object' && urlItem.value) { const title = await getWebsiteTitle(urlItem.value); setPlatformTitle(title); } @@ -25,15 +65,16 @@ const Details = ({ data }: { data: any }) => { } }; - if (!collaborative) return; - - if (platformUrl === null) { - setPlatformTitle('N/A'); - } else { - fetchTitle(); - } + fetchTitle(); }, [collaborative, platformUrl]); + const platformHref = + typeof platformUrl === 'string' + ? platformUrl + : platformUrl && typeof platformUrl === 'object' + ? platformUrl.value + : undefined; + const PrimaryDetails = [ { label: 'Collaborative Name', value: collaborative?.title }, { label: 'Summary', value: collaborative?.summary }, @@ -50,7 +91,7 @@ const Details = ({ data }: { data: any }) => { label: 'Sectors', value: collaborative?.sectors?.length ? (
- {collaborative.sectors.map((s: any, idx: number) => ( + {collaborative.sectors.map((s, idx) => ( {s?.name} ))}
@@ -60,7 +101,7 @@ const Details = ({ data }: { data: any }) => { label: 'SDG Goals', value: collaborative?.sdgs?.length ? (
- {collaborative.sdgs.map((s: any, idx: number) => ( + {collaborative.sdgs.map((s, idx) => ( {s?.name || s?.code} ))}
@@ -70,13 +111,13 @@ const Details = ({ data }: { data: any }) => { label: 'Tags', value: collaborative?.tags?.length ? (
- {collaborative.tags.map((t: any, idx: number) => ( + {collaborative.tags.map((t, idx) => ( {t?.value} ))}
) : null, }, - ...(collaborative?.metadata?.map((meta: any) => ({ + ...(collaborative?.metadata?.map((meta) => ({ label: meta.metadataItem?.label, value: meta.value, })) || []), @@ -94,7 +135,7 @@ const Details = ({ data }: { data: any }) => {
{item.label === 'Summary' ? ( ) : ( @@ -112,7 +153,7 @@ const Details = ({ data }: { data: any }) => { ) : null )} - {data.collaboratives[0].platformUrl && ( + {platformHref && (
External Link: @@ -120,7 +161,7 @@ const Details = ({ data }: { data: any }) => {
{
)} - {data?.collaboratives[0]?.logo && ( + {collaborative?.logo && (
@@ -142,15 +183,15 @@ const Details = ({ data }: { data: any }) => {
)} - {data?.collaboratives[0]?.coverImage && ( + {collaborative?.coverImage && (
@@ -158,8 +199,8 @@ const Details = ({ data }: { data: any }) => {
{ entitySlug: string; id: string; }>(); - const CollaborativeData: { data: any; isLoading: boolean; refetch: any } = + const CollaborativeData = useQuery( [ `fetch_CollaborativeDetails`, @@ -177,9 +177,9 @@ const Publish = () => { `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives` ); }, - onError: (err: any) => { + onError: (err: unknown) => { const errorMessage = - typeof err?.message === 'string' && err.message.trim() + typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' && err.message.trim() ? err.message.trim() : 'Unable to publish collaborative right now. Please try again.'; toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID }); @@ -192,27 +192,27 @@ const Publish = () => { name: 'Details', data: CollaborativeData.data?.collaboratives, error: - CollaborativeData.data?.collaboratives[0]?.sectors.length === 0 || - CollaborativeData.data?.collaboratives[0]?.summary.length === 0 || - CollaborativeData.data?.collaboratives[0]?.sdgs.length === 0 || - CollaborativeData.data?.collaboratives[0]?.logo === null || - CollaborativeData.data?.collaboratives[0]?.coverImage === null + CollaborativeData.data?.collaboratives?.[0]?.sectors?.length === 0 || + CollaborativeData.data?.collaboratives?.[0]?.summary?.length === 0 || + CollaborativeData.data?.collaboratives?.[0]?.sdgs?.length === 0 || + CollaborativeData.data?.collaboratives?.[0]?.logo === null || + CollaborativeData.data?.collaboratives?.[0]?.coverImage === null ? 'Summary, SDG, Sectors, Logo, or Cover Image is missing. Please add to continue.' : '', errorType: 'critical', }, { name: 'Datasets', - data: CollaborativeData?.data?.collaboratives[0]?.datasets, + data: CollaborativeData?.data?.collaboratives?.[0]?.datasets, error: CollaborativeData.data && - CollaborativeData.data?.collaboratives[0]?.datasets.length === 0 + CollaborativeData.data?.collaboratives?.[0]?.datasets?.length === 0 ? 'No datasets assigned. Please assign to continue.' : '', }, { name: 'Use Cases', - data: CollaborativeData?.data?.collaboratives[0]?.useCases, + data: CollaborativeData?.data?.collaboratives?.[0]?.useCases, error: '', }, // { @@ -222,21 +222,31 @@ const Publish = () => { // }, { name: 'Contributors', - data: CollaborativeData?.data?.collaboratives[0]?.length > 0, + data: CollaborativeData?.data?.collaboratives?.[0] != null && + 'length' in CollaborativeData.data.collaboratives[0] && + typeof CollaborativeData.data.collaboratives[0].length === 'number' && + CollaborativeData.data.collaboratives[0].length > 0, error: '', }, ]; - const isPublishDisabled = (collaborative: any) => { + const isPublishDisabled = (collaborative: { + datasets?: unknown[] | null; + sectors?: unknown[] | null; + summary?: string | null; + sdgs?: unknown[] | null; + logo?: unknown; + coverImage?: unknown; + } | null | undefined) => { if (!collaborative) return true; - const hasDatasets = collaborative?.datasets.length > 0; + const hasDatasets = (collaborative.datasets?.length ?? 0) > 0; const hasRequiredMetadata = - collaborative.sectors.length > 0 && - collaborative?.summary.length > 0 && - collaborative?.sdgs.length > 0 && - collaborative?.logo !== null && - collaborative?.coverImage !== null; + (collaborative.sectors?.length ?? 0) > 0 && + (collaborative.summary?.length ?? 0) > 0 && + (collaborative.sdgs?.length ?? 0) > 0 && + collaborative.logo !== null && + collaborative.coverImage !== null; // No datasets assigned if (!hasDatasets) return true; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx index 395d8eb8..f0ce441f 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/[id]/usecases/page.tsx @@ -11,8 +11,22 @@ import { GraphQL } from '@/lib/api'; import { formatDate } from '@/lib/utils'; import { Loading } from '@/components/loading'; +interface SearchUseCase { + id: string | number; + title: string; + modified?: string; + sectors?: Array<{ name?: string } | string>; +} + +interface AssignTableRow { + id: string; + title: string; + category: string; + modified: string; +} + // prettier-ignore -const FetchCollaborativeDetails: any = graphql(` +const FetchCollaborativeDetails = graphql(` query CollaborativeUseCaseDetails($filters: CollaborativeFilter) { collaboratives(filters: $filters) { id @@ -31,7 +45,7 @@ const FetchCollaborativeDetails: any = graphql(` `); // prettier-ignore -const AssignCollaborativeUseCases: any = graphql(` +const AssignCollaborativeUseCases = graphql(` mutation assignCollaborativeUseCases($collaborativeId: String!, $useCaseIds: [String!]!) { updateCollaborativeUseCases(collaborativeId: $collaborativeId, useCaseIds: $useCaseIds) { ... on TypeCollaborative { @@ -55,10 +69,10 @@ const UseCases = () => { const queryClient = useQueryClient(); const COLLAB_USECASES_TOAST_ID = 'collaboratives-usecases-toast'; - const [data, setData] = useState([]); // Ensure `data` is an array - const [selectedRow, setSelectedRows] = useState([]); + const [data, setData] = useState([]); + const [selectedRow, setSelectedRows] = useState([]); - const CollaborativeDetails: { data: any; isLoading: boolean; refetch: any } = + const CollaborativeDetails = useQuery( [`Collaborative_UseCase_Details`, params.id], () => @@ -81,7 +95,7 @@ const UseCases = () => { useEffect(() => { fetchData('usecase', '?size=1000&page=1') - .then((res) => { + .then((res: { results: SearchUseCase[] }) => { setData(res.results); }) .catch((err) => { @@ -95,22 +109,22 @@ const UseCases = () => { { accessorKey: 'modified', header: 'Last Modified' }, ]; - const generateTableData = (list: Array) => { + const generateTableData = (list: SearchUseCase[]) => { return list.map((item) => { const sector = item.sectors?.[0]; return { title: item.title, id: String(item.id), category: typeof sector === 'string' ? sector : sector?.name || 'N/A', - modified: formatDate(item.modified) || '', + modified: formatDate(item.modified ?? null) || '', }; }); }; const rows = generateTableData(data); const assignedUseCaseIds = new Set( - (CollaborativeDetails?.data?.collaboratives[0]?.useCases ?? []).map( - (item: any) => String(item.id) + (CollaborativeDetails?.data?.collaboratives?.[0]?.useCases ?? []).map( + (item) => String(item.id) ) ); const defaultSelectedRows = rows.filter((row) => @@ -127,7 +141,7 @@ const UseCases = () => { { collaborativeId: params.id, useCaseIds: Array.isArray(selectedRow) - ? selectedRow.map((row: any) => String(row.id)) + ? selectedRow.map((row) => String(row.id)) : [], } ), @@ -151,7 +165,7 @@ const UseCases = () => { `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives/edit/${params.id}/contributors` ); }, - onError: (err: any) => { + onError: (err: unknown) => { toast(`Received ${err} on use case assignment`, { id: COLLAB_USECASES_TOAST_ID, }); @@ -161,7 +175,7 @@ const UseCases = () => { return ( <> - {CollaborativeDetails?.data?.collaboratives[0]?.useCases?.length >= 0 && + {((CollaborativeDetails?.data?.collaboratives?.[0]?.useCases?.length ?? -1) >= 0) && data.length > 0 && !CollaborativeDetails.isLoading ? ( <> diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx index d2e747b6..458d34f1 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/edit/layout.tsx @@ -11,7 +11,7 @@ import StepNavigation from '../../components/StepNavigation'; import TitleBar from '../../components/title-bar'; import { EditStatusProvider, useEditStatus } from './context'; -const UpdateCollaborativeTitleMutation: any = graphql(` +const UpdateCollaborativeTitleMutation = graphql(` mutation updateCollaborativeTitle($data: CollaborativeInputPartial!) { updateCollaborative(data: $data) { __typename @@ -21,7 +21,7 @@ const UpdateCollaborativeTitleMutation: any = graphql(` } `); -const FetchCollaborativeTitle: any = graphql(` +const FetchCollaborativeTitle = graphql(` query CollaborativeTitle($pk: ID!) { collaborative(pk: $pk) { id @@ -52,12 +52,7 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => { return pathName.indexOf(v) >= 0; }); - const CollaborativeData: { - data: any; - isLoading: boolean; - error: any; - refetch: any; - } = useQuery( + const CollaborativeData = useQuery( [`fetch_CollaborativeData_${params.id}`], () => GraphQL( @@ -111,8 +106,8 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => { ], }); }, - onError: (error: any) => { - toast(`Error: ${error.message}`, { id: COLLAB_EDIT_TOAST_ID }); + onError: (error: unknown) => { + toast(`Error: ${typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' ? error.message : String(error)}`, { id: COLLAB_EDIT_TOAST_ID }); }, } ); @@ -174,7 +169,9 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
Error loading collaborative data
- {CollaborativeData.error?.message || 'Unknown error'} + {CollaborativeData.error instanceof Error + ? CollaborativeData.error.message + : 'Unknown error'}
Check console for details. ID: {params.id} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx index 4a84d60a..2c9bab54 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/collaboratives/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { graphql } from '@/gql'; +import { CollaborativeStatus, Ordering } from '@/gql/generated/graphql'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import { parseAsString, useQueryState } from 'nuqs'; @@ -16,7 +17,21 @@ import { formatDate } from '@/lib/utils'; import { ActionBar } from '../dataset/components/action-bar'; import { Navigation } from '../dataset/components/navigate-org-datasets'; -const allCollaboratives: any = graphql(` +interface CollaborativeListItem { + id: string; + title?: string | null; + created?: string | null; + modified?: string | null; +} + +interface CollaborativeTableRow { + id: string; + title?: string | null; + created: string; + modified: string; +} + +const allCollaboratives = graphql(` query CollaborativesData( $filters: CollaborativeFilter $order: CollaborativeOrder @@ -30,13 +45,13 @@ const allCollaboratives: any = graphql(` } `); -const deleteCollaborative: any = graphql(` +const deleteCollaborative = graphql(` mutation deleteCollaborative($collaborativeId: String!) { deleteCollaborative(collaborativeId: $collaborativeId) } `); -const AddCollaborative: any = graphql(` +const AddCollaborative = graphql(` mutation AddCollaborative { addCollaborative { __typename @@ -48,7 +63,7 @@ const AddCollaborative: any = graphql(` } `); -const unPublishCollaborative: any = graphql(` +const unPublishCollaborative = graphql(` mutation unPublishCollaborativeMutation($collaborativeId: String!) { unpublishCollaborative(collaborativeId: $collaborativeId) { __typename @@ -76,7 +91,7 @@ export default function CollaborativePage() { const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString); - const AllCollaboratives: { data: any; isLoading: boolean; refetch: any } = + const AllCollaboratives = useQuery( [ 'fetch_Collaboratives', @@ -87,9 +102,9 @@ export default function CollaborativePage() { () => GraphQL(allCollaboratives, ownerArgs || {}, { filters: { - status: navigationTab === 'published' ? 'PUBLISHED' : 'DRAFT', + status: navigationTab === 'published' ? CollaborativeStatus.Published : CollaborativeStatus.Draft, }, - order: { modified: 'DESC' }, + order: { modified: Ordering.Desc }, }), { enabled: isValidParams } ); @@ -104,11 +119,7 @@ export default function CollaborativePage() { const COLLAB_LIST_TOAST_ID = 'collaboratives-list-toast'; - const DeleteCollaborativeMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const DeleteCollaborativeMutation = useMutation( [`delete_Collaborative`], (data: { id: string }) => GraphQL(deleteCollaborative, ownerArgs || {}, { @@ -121,40 +132,34 @@ export default function CollaborativePage() { AllCollaboratives.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0], { id: COLLAB_LIST_TOAST_ID }); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0], { id: COLLAB_LIST_TOAST_ID }); }, } ); - const CreateCollaborative: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const CreateCollaborative = useMutation( [`create_Collaborative`], - () => GraphQL(AddCollaborative, ownerArgs || {}, []), + () => GraphQL(AddCollaborative, ownerArgs || {}), { - onSuccess: (response: any) => { + onSuccess: (response) => { toast(`Collaborative created successfully`, { id: COLLAB_LIST_TOAST_ID }); if (isValidParams && entityType && entitySlug) { + const created = response.addCollaborative; + const createdId = 'id' in created ? created.id : undefined; router.push( - `/dashboard/${entityType}/${entitySlug}/collaboratives/edit/${response.addCollaborative.id}/details` + `/dashboard/${entityType}/${entitySlug}/collaboratives/edit/${createdId}/details` ); AllCollaboratives.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0], { id: COLLAB_LIST_TOAST_ID }); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0], { id: COLLAB_LIST_TOAST_ID }); }, } ); - const UnpublishCollaborativeMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const UnpublishCollaborativeMutation = useMutation( [`unpublish_collaborative`], (data: { id: string }) => GraphQL(unPublishCollaborative, ownerArgs || {}, { @@ -167,8 +172,8 @@ export default function CollaborativePage() { AllCollaboratives.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0], { id: COLLAB_LIST_TOAST_ID }); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0], { id: COLLAB_LIST_TOAST_ID }); }, } ); @@ -177,7 +182,7 @@ export default function CollaborativePage() { return null; } - let navigationOptions = [ + const navigationOptions = [ { label: 'Drafts', url: `drafts`, @@ -194,11 +199,11 @@ export default function CollaborativePage() { { accessorKey: 'title', header: 'Title', - cell: ({ row }: any) => + cell: ({ row }: { row: { original: CollaborativeTableRow } }) => navigationTab === 'published' ? ( {row.original.title} @@ -219,7 +224,7 @@ export default function CollaborativePage() { { accessorKey: 'delete', header: 'Delete', - cell: ({ row }: any) => + cell: ({ row }: { row: { original: CollaborativeTableRow } }) => navigationTab === 'published' ? (
{accessModelList?.accessModelResources.map( - (item: any, index: any) => ( + (item, index) => (
= ({ helpText={ 'Only Resources added will be part of this Access Type. After adding select the Fields and Rows to be included' } - onChange={(e: any) => handleAddResource(e)} + onChange={(value) => { + if (Array.isArray(value)) { + handleAddResource(value); + } + }} />
@@ -527,9 +672,9 @@ const AccessModelForm: React.FC = ({
- {selectedResources?.map((resourceId: any, index) => { + {selectedResources?.map((resourceId, index) => { const selectedResource = data?.datasetResources.find( - (resource: any) => resource.id === resourceId.value + (resource) => resource.id === resourceId.value ); if (!selectedResource || !selectedResource.schema) { diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx index 752f5492..aae782fb 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/AccessModelList.tsx @@ -1,4 +1,3 @@ -import { UUID } from 'crypto'; import React, { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; @@ -18,12 +17,30 @@ import { formatDate, toTitleCase } from '@/lib/utils'; import { Icons } from '@/components/icons'; interface AccessModelListProps { - setList: any; - list: any; - setAccessModelId: any; + setList: (list: boolean) => void; + list: boolean; + setAccessModelId: (id: string | null) => void; } -const accessModelQuery: any = graphql(` +interface AccessModelRow { + id: string; + name: string; + type: string; + created?: string | null; +} + +interface AccessModelTableRow { + name: string; + date?: string | null; + type: string; + id: string; +} + +interface AccessModelTableCell { + row: { original: AccessModelTableRow }; +} + +const accessModelQuery = graphql(` query accessModelResources($datasetId: UUID!) { accessModelResources(datasetId: $datasetId) { id @@ -36,7 +53,7 @@ const accessModelQuery: any = graphql(` } `); -const deleteAccessModel: any = graphql(` +const deleteAccessModel = graphql(` mutation deleteAccessModel($accessModelId: UUID!) { deleteAccessModel(accessModelId: $accessModelId) } @@ -48,11 +65,12 @@ const AccessModelList: React.FC = ({ setAccessModelId, }) => { const ACCESS_MODEL_DELETE_ERROR_TOAST_ID = 'dataset-access-model-delete-error'; - const getErrorMessage = ( - err: any, - fallback: string - ) => - typeof err?.message === 'string' && err.message.trim() + const getErrorMessage = (err: unknown, fallback: string) => + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + err.message.trim() ? err.message.trim() : fallback; @@ -62,11 +80,7 @@ const AccessModelList: React.FC = ({ id: string; }>(); - const { - data, - isLoading, - refetch, - }: { data: any; isLoading: boolean; refetch: any } = useQuery( + const { data, isLoading, refetch } = useQuery( [`accessModelList_${params.id}`], () => GraphQL( @@ -80,17 +94,25 @@ const AccessModelList: React.FC = ({ ) ); - const [filteredRows, setFilteredRows] = useState([]); - - useEffect(() => { - refetch(); + const [filteredRows, setFilteredRows] = useState([]); + const [prevAccessModels, setPrevAccessModels] = useState( + data?.accessModelResources + ); + if (data?.accessModelResources !== prevAccessModels) { + setPrevAccessModels(data?.accessModelResources); if (data?.accessModelResources) { setFilteredRows(data.accessModelResources); } + } + + useEffect(() => { + refetch(); }, [data, list, refetch]); const { mutate, isLoading: deleteLoading } = useMutation( - (data: { accessModelId: UUID }) => + (data: { + accessModelId: `${string}-${string}-${string}-${string}-${string}`; + }) => GraphQL( deleteAccessModel, { @@ -103,7 +125,7 @@ const AccessModelList: React.FC = ({ toast('Access Model Deleted Successfully'); refetch(); }, - onError: (err: any) => { + onError: (err: unknown) => { toast( `Error: ${getErrorMessage(err, 'Unable to delete access model right now.')}`, { id: ACCESS_MODEL_DELETE_ERROR_TOAST_ID } @@ -112,7 +134,7 @@ const AccessModelList: React.FC = ({ } ); - const handleAccessModel = (row: any) => { + const handleAccessModel = (row: AccessModelTableCell['row']) => { setAccessModelId(row.original.id); setList(false); }; @@ -122,7 +144,7 @@ const AccessModelList: React.FC = ({ { accessorKey: 'name', header: 'Name of Access Type', - cell: ({ row }: any) => ( + cell: ({ row }: AccessModelTableCell) => (
handleAccessModel(row)} @@ -134,8 +156,8 @@ const AccessModelList: React.FC = ({ { accessorKey: 'date', header: 'Date Added', - cell: ({ row }: any) => { - return {formatDate(row.original.date) || ''}; + cell: ({ row }: AccessModelTableCell) => { + return {formatDate(row.original.date ?? null) || ''}; }, }, { @@ -144,13 +166,18 @@ const AccessModelList: React.FC = ({ }, { header: 'DELETE', - cell: ({ row }: any) => ( + cell: ({ row }: AccessModelTableCell) => (
mutate({ accessModelId: row.original.id })} + onClick={() => + mutate({ + accessModelId: + row.original.id as `${string}-${string}-${string}-${string}-${string}`, + }) + } > Delete @@ -160,18 +187,23 @@ const AccessModelList: React.FC = ({ ]; }; - const generateTableData = (accessModel: any[]) => { - return accessModel?.map((item: any) => ({ - name: item.name, - date: item.created, - type: toTitleCase(item.type.split('.').pop().toLowerCase()), - id: item.id, - })); + const generateTableData = (accessModel: AccessModelRow[]) => { + return accessModel?.map((item) => { + const permission = item.type.split('.').pop(); + return { + name: item.name, + date: item.created, + type: permission + ? toTitleCase(permission.toLowerCase()) + : item.type, + id: item.id, + }; + }); }; const handleSearchChange = (e: string) => { const searchTerm = e.toLowerCase(); - const filtered = data?.accessModelResources.filter((row: any) => + const filtered = data?.accessModelResources.filter((row) => row.name.toLowerCase().includes(searchTerm) ); setFilteredRows(filtered || []); @@ -194,7 +226,7 @@ const AccessModelList: React.FC = ({ placeholder="Search in Resources" label="Search" name="Search" - onChange={(e) => handleSearchChange(e)} + onChange={(search) => handleSearchChange(search)} />
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx index 40183250..8f338b26 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditDataset.tsx @@ -1,18 +1,27 @@ import React from 'react'; +import { PatchDataset } from '@/types'; import { CreateDataset } from '../../../new/components/new-dataset'; +interface EditDatasetProps { + defaultVal: PatchDataset; + submitRef: React.RefObject; + mutate: (res: { + dataset_data: { + title: string; + description: string; + id?: string; + }; + }) => void; + isLoading: boolean; +} + export function EditDataset({ defaultVal, submitRef, mutate, isLoading, -}: { - defaultVal: any; - submitRef: React.RefObject; - mutate: any; - isLoading: boolean; -}) { +}: EditDatasetProps) { return ( - typeof err?.message === 'string' && err.message.trim() + const getErrorMessage = (err: unknown, fallback: string) => + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + err.message.trim() ? err.message.trim() : fallback; @@ -72,8 +73,7 @@ export function EditLayout({ children, params }: LayoutProps) { const [, setEditMode] = useState(false); - const getDatasetTitleRes: { data: any; isLoading: boolean; refetch: any } = - useQuery([`dataset_title_${routerParams.id}`], () => + const getDatasetTitleRes = useQuery([`dataset_title_${routerParams.id}`], () => GraphQL( datasetQueryDoc, { @@ -106,7 +106,7 @@ export function EditLayout({ children, params }: LayoutProps) { getDatasetTitleRes.refetch(); }, - onError: (err: any) => { + onError: (err: unknown) => { toast(getErrorMessage(err, 'Unable to update dataset title right now.'), { id: DATASET_TITLE_SAVE_ERROR_TOAST_ID, }); @@ -132,7 +132,7 @@ export function EditLayout({ children, params }: LayoutProps) { ) : ( updateDatasetTitleMutation.mutate({ @@ -186,7 +186,7 @@ const Navigation = ({ }) => { const router = useRouter(); - let links = [ + const links = [ { label: 'Metadata', id: 'metadata', @@ -225,6 +225,11 @@ const Navigation = ({ ]; const [selectedTab, setSelectedTab] = useState(pathItem || 'distributions'); + const [prevPathItem, setPrevPathItem] = useState(pathItem); + if (pathItem !== prevPathItem) { + setPrevPathItem(pathItem); + setSelectedTab(pathItem); + } const handleTabClick = (item: { label: string; @@ -238,10 +243,6 @@ const Navigation = ({ } }; - useEffect(() => { - setSelectedTab(pathItem); // Update selected tab on path change - }, [pathItem]); - return (
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx index 2520816c..e258be5a 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/EditMetadata.tsx @@ -4,11 +4,9 @@ import { useEffect, useRef, useState } from 'react'; import { useParams } from 'next/navigation'; import { graphql } from '@/gql'; import { - TypeDataset, - TypeMetadata, - TypeSector, - TypeTag, + MetadataModels, UpdateMetadataInput, + UpdatePromptMetadataInput, } from '@/gql/generated/graphql'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { @@ -27,7 +25,7 @@ import { RichTextEditor } from '@/components/RichTextEditor'; import DatasetLoading from '../../../components/loading-dataset'; import { useDatasetEditStatus } from '../context'; -const sectorsListQueryDoc: any = graphql(` +const sectorsListQueryDoc = graphql(` query SectorList { sectors { id @@ -36,7 +34,7 @@ const sectorsListQueryDoc: any = graphql(` } `); -const tagsListQueryDoc: any = graphql(` +const tagsListQueryDoc = graphql(` query TagsList { tags { id @@ -45,7 +43,7 @@ const tagsListQueryDoc: any = graphql(` } `); -const geographiesListQueryDoc: any = graphql(` +const geographiesListQueryDoc = graphql(` query GeographiesList { geographies { id @@ -60,7 +58,7 @@ const geographiesListQueryDoc: any = graphql(` } `); -const datasetMetadataQueryDoc: any = graphql(` +const datasetMetadataQueryDoc = graphql(` query MetadataValues($filters: DatasetFilter) { datasets(filters: $filters) { title @@ -97,7 +95,7 @@ const datasetMetadataQueryDoc: any = graphql(` } `); -const metadataQueryDoc: any = graphql(` +const metadataQueryDoc = graphql(` query MetaDataList($filters: MetadataFilter) { metadata(filters: $filters) { id @@ -116,7 +114,7 @@ const metadataQueryDoc: any = graphql(` `); // Introspection query to get PromptTaskType enum values from schema -const promptTaskTypeEnumQuery: any = graphql(` +const promptTaskTypeEnumQuery = graphql(` query PromptTaskTypeEnum { __type(name: "PromptTaskType") { enumValues { @@ -128,7 +126,7 @@ const promptTaskTypeEnumQuery: any = graphql(` `); // Introspection query to get PromptDomain enum values from schema -const promptDomainEnumQuery: any = graphql(` +const promptDomainEnumQuery = graphql(` query PromptDomainEnum { __type(name: "PromptDomain") { enumValues { @@ -140,7 +138,7 @@ const promptDomainEnumQuery: any = graphql(` `); // Introspection query to get TargetLanguage enum values from schema -const targetLanguageEnumQuery: any = graphql(` +const targetLanguageEnumQuery = graphql(` query TargetLanguageEnum { __type(name: "TargetLanguage") { enumValues { @@ -152,7 +150,7 @@ const targetLanguageEnumQuery: any = graphql(` `); // Introspection query to get TargetModelType enum values from schema -const targetModelTypeEnumQuery: any = graphql(` +const targetModelTypeEnumQuery = graphql(` query TargetModelTypeEnum { __type(name: "TargetModelType") { enumValues { @@ -164,7 +162,7 @@ const targetModelTypeEnumQuery: any = graphql(` `); // Mutation to update prompt-specific metadata -const updatePromptMetadataMutationDoc: any = graphql(` +const updatePromptMetadataMutationDoc = graphql(` mutation UpdatePromptMetadata($updateInput: UpdatePromptMetadataInput!) { updatePromptMetadata(updateInput: $updateInput) { success @@ -185,7 +183,7 @@ const updatePromptMetadataMutationDoc: any = graphql(` } `); -const updateMetadataMutationDoc: any = graphql(` +const updateMetadataMutationDoc = graphql(` mutation SaveMetadata($UpdateMetadataInput: UpdateMetadataInput!) { addUpdateDatasetMetadata(updateMetadataInput: $UpdateMetadataInput) { success @@ -230,6 +228,70 @@ const updateMetadataMutationDoc: any = graphql(` } `); +interface DatasetMetadataSource { + description?: string | null; + license?: string | null; + metadata?: Array<{ + value?: string | null; + metadataItem: { id: string; dataType: string }; + }> | null; + sectors?: Array<{ id: string; name?: string | null }> | null; + tags?: Array<{ id: string; value?: string | null }> | null; + geographies?: Array<{ id: string; name?: string | null }> | null; +} + +interface MetadataFormItem { + id: string; + label: string; + dataType: string; + options?: string[] | null; + enabled?: boolean | null; + value?: string | null; +} + +interface OptionItem { + label: string; + value: string; +} + +type FormFieldValue = + | string + | number + | boolean + | null + | OptionItem + | OptionItem[]; + +interface MetadataFormData { + [key: string]: FormFieldValue; + description: string; + sectors: OptionItem[]; + license: string | null; + tags: OptionItem[]; + geographies: OptionItem[]; + isPublic: boolean; +} + +function optionValue(item: unknown): unknown { + if (typeof item === 'object' && item !== null && 'value' in item) { + return item.value; + } + return item; +} + +function asOptionItems(value: FormFieldValue | undefined): OptionItem[] { + if (!Array.isArray(value)) { + return []; + } + return value.filter( + (item): item is OptionItem => + typeof item === 'object' && + item !== null && + 'label' in item && + 'value' in item + ); +} + export function EditMetadata({ id }: { id: string }) { const params = useParams<{ entityType: string; @@ -242,17 +304,16 @@ export function EditMetadata({ id }: { id: string }) { const PROMPT_METADATA_ERROR_TOAST_ID = 'dataset-prompt-metadata-error'; const DATASET_METADATA_SUCCESS_TOAST_ID = 'dataset-metadata-save-success'; const DATASET_METADATA_ERROR_TOAST_ID = 'dataset-metadata-save-error'; - const getErrorMessage = (err: any, fallback: string) => - typeof err?.message === 'string' && err.message.trim() + const getErrorMessage = (err: unknown, fallback: string) => + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + err.message.trim() ? err.message.trim() : fallback; - const getDatasetMetadata: { - data: any; - isLoading: boolean; - refetch: any; - error: any; - } = useQuery( + const getDatasetMetadata = useQuery( [`metadata_values_query_${params.id}`], () => GraphQL( @@ -268,48 +329,25 @@ export function EditMetadata({ id }: { id: string }) { } ); - const getSectorsList: { data: any; isLoading: boolean; error: any } = - useQuery([`sectors_list_query`], () => - GraphQL( - sectorsListQueryDoc, - { - [params.entityType]: params.entitySlug, - }, - [] - ) - ); - - const getTagsList: { - data: any; - isLoading: boolean; - error: any; - refetch: any; - } = useQuery([`tags_list_query`], () => - GraphQL( - tagsListQueryDoc, - { - [params.entityType]: params.entitySlug, - }, - [] - ) + const getSectorsList = useQuery([`sectors_list_query`], () => + GraphQL(sectorsListQueryDoc, { + [params.entityType]: params.entitySlug, + }) ); - const getGeographiesList: { data: any; isLoading: boolean; error: any } = - useQuery([`geographies_list_query`], () => - GraphQL( - geographiesListQueryDoc, - { - [params.entityType]: params.entitySlug, - }, - [] - ) - ); - - const getMetaDataListQuery: { - data: any; - isLoading: boolean; - refetch: any; - } = useQuery([`metadata_fields_list_${id}`], () => + const getTagsList = useQuery([`tags_list_query`], () => + GraphQL(tagsListQueryDoc, { + [params.entityType]: params.entitySlug, + }) + ); + + const getGeographiesList = useQuery([`geographies_list_query`], () => + GraphQL(geographiesListQueryDoc, { + [params.entityType]: params.entitySlug, + }) + ); + + const getMetaDataListQuery = useQuery([`metadata_fields_list_${id}`], () => GraphQL( metadataQueryDoc, { @@ -317,7 +355,7 @@ export function EditMetadata({ id }: { id: string }) { }, { filters: { - model: 'DATASET', + model: 'DATASET' as MetadataModels, enabled: true, }, } @@ -325,30 +363,27 @@ export function EditMetadata({ id }: { id: string }) { ); // Fetch PromptTaskType enum values from GraphQL schema - const getPromptTaskTypeEnum: { data: any; isLoading: boolean } = useQuery( + const getPromptTaskTypeEnum = useQuery( ['prompt_task_type_enum'], - () => GraphQL(promptTaskTypeEnumQuery, {}, []), - { staleTime: Infinity } // Enum values don't change, cache indefinitely + () => GraphQL(promptTaskTypeEnumQuery), + { staleTime: Infinity } ); - // Fetch PromptDomain enum values from GraphQL schema - const getPromptDomainEnum: { data: any; isLoading: boolean } = useQuery( + const getPromptDomainEnum = useQuery( ['prompt_domain_enum'], - () => GraphQL(promptDomainEnumQuery, {}, []), + () => GraphQL(promptDomainEnumQuery), { staleTime: Infinity } ); - // Fetch TargetLanguage enum values from GraphQL schema - const getTargetLanguageEnum: { data: any; isLoading: boolean } = useQuery( + const getTargetLanguageEnum = useQuery( ['target_language_enum'], - () => GraphQL(targetLanguageEnumQuery, {}, []), + () => GraphQL(targetLanguageEnumQuery), { staleTime: Infinity } ); - // Fetch TargetModelType enum values from GraphQL schema - const getTargetModelTypeEnum: { data: any; isLoading: boolean } = useQuery( + const getTargetModelTypeEnum = useQuery( ['target_model_type_enum'], - () => GraphQL(targetModelTypeEnumQuery, {}, []), + () => GraphQL(targetModelTypeEnumQuery), { staleTime: Infinity } ); @@ -362,22 +397,9 @@ export function EditMetadata({ id }: { id: string }) { targetModelTypes?: string[]; }>({}); - // Initialize prompt metadata state when data loads - useEffect(() => { - const promptMeta = getDatasetMetadata.data?.datasets?.[0]?.promptMetadata; - if (promptMeta) { - setPromptMetadataState({ - taskType: promptMeta.task_type || undefined, - domain: promptMeta.domain || undefined, - targetLanguages: promptMeta.target_languages || [], - targetModelTypes: promptMeta.target_model_types || [], - }); - } - }, [getDatasetMetadata.data?.datasets]); - // Mutation for updating prompt metadata const updatePromptMetadataMutation = useMutation( - (data: { updateInput: any }) => + (data: { updateInput: UpdatePromptMetadataInput }) => GraphQL( updatePromptMetadataMutationDoc, { @@ -386,7 +408,7 @@ export function EditMetadata({ id }: { id: string }) { data ), { - onSuccess: (res: any) => { + onSuccess: (res) => { if (res.updatePromptMetadata.success) { toast('Prompt metadata updated successfully!', { id: PROMPT_METADATA_SUCCESS_TOAST_ID, @@ -404,7 +426,7 @@ export function EditMetadata({ id }: { id: string }) { }); } }, - onError: (err: any) => { + onError: (err: unknown) => { toast( `Error: ${getErrorMessage(err, 'Unable to update prompt metadata right now. Please try again.')}`, { id: PROMPT_METADATA_ERROR_TOAST_ID } @@ -421,8 +443,8 @@ export function EditMetadata({ id }: { id: string }) { updatePromptMetadataMutation.mutate({ updateInput: { dataset: params.id, - taskType: newState.taskType, - domain: newState.domain, + taskType: newState.taskType as UpdatePromptMetadataInput['taskType'], + domain: newState.domain as UpdatePromptMetadataInput['domain'], targetLanguages: newState.targetLanguages, targetModelTypes: newState.targetModelTypes, }, @@ -439,7 +461,7 @@ export function EditMetadata({ id }: { id: string }) { data ), { - onSuccess: (res: any) => { + onSuccess: (res) => { if (res.addUpdateDatasetMetadata.success) { toast('Details updated successfully!', { id: DATASET_METADATA_SUCCESS_TOAST_ID, @@ -451,7 +473,7 @@ export function EditMetadata({ id }: { id: string }) { queryKey: [`metadata_fields_list_${id}`], }); const updatedData = defaultValuesPrepFn( - res.addUpdateDatasetMetadata.data + res.addUpdateDatasetMetadata.data ?? undefined ); if (isTagsListUpdated) { getTagsList.refetch(); @@ -470,7 +492,7 @@ export function EditMetadata({ id }: { id: string }) { }); } }, - onError: (err: any) => { + onError: (err: unknown) => { toast( `Error: ${getErrorMessage(err, 'Unable to update details right now. Please try again.')}`, { id: DATASET_METADATA_ERROR_TOAST_ID } @@ -479,27 +501,27 @@ export function EditMetadata({ id }: { id: string }) { } ); - const defaultValuesPrepFn = (dataset?: TypeDataset) => { - let defaultVal: { - [key: string]: any; - } = {}; + const defaultValuesPrepFn = ( + dataset?: DatasetMetadataSource + ): MetadataFormData => { + const defaultVal: MetadataFormData = { + description: '', + sectors: [], + license: null, + tags: [], + geographies: [], + isPublic: true, + }; if (!dataset) { - return { - description: '', - sectors: [], - license: null, - tags: [], - geographies: [], - isPublic: true, - }; + return defaultVal; } if ((dataset?.metadata || []).length > 0) { (dataset?.metadata || []).map((field) => { if ( field.metadataItem.dataType === 'MULTISELECT' && - field.value !== '' + field.value ) { defaultVal[field.metadataItem.id] = field.value .split(', ') @@ -518,9 +540,9 @@ export function EditMetadata({ id }: { id: string }) { defaultVal['description'] = dataset?.description || ''; defaultVal['sectors'] = - dataset?.sectors?.map((sector: TypeSector) => { + dataset?.sectors?.map((sector) => { return { - label: sector.name, + label: sector.name || '', value: sector.id, }; }) || []; @@ -528,17 +550,17 @@ export function EditMetadata({ id }: { id: string }) { defaultVal['license'] = dataset?.license || null; defaultVal['tags'] = - dataset?.tags?.map((tag: TypeTag) => { + dataset?.tags?.map((tag) => { return { - label: tag.value, + label: tag.value || '', value: tag.id, }; }) || []; defaultVal['geographies'] = - dataset?.geographies?.map((geo: any) => { + dataset?.geographies?.map((geo) => { return { - label: geo.name, + label: geo.name || '', value: geo.id, }; }) || []; @@ -549,25 +571,37 @@ export function EditMetadata({ id }: { id: string }) { }; const [formData, setFormData] = useState( - defaultValuesPrepFn( - getDatasetMetadata?.data?.datasets?.[0] || ({} as TypeDataset) - ) + defaultValuesPrepFn(getDatasetMetadata?.data?.datasets?.[0]) ); const [previousFormData, setPreviousFormData] = useState(formData); const formDataRef = useRef(formData); - - useEffect(() => { - if (getDatasetMetadata.data?.datasets[0]) { - const updatedData = defaultValuesPrepFn( - getDatasetMetadata.data.datasets[0] - ); + const [prevMetadataData, setPrevMetadataData] = useState( + getDatasetMetadata.data + ); + if (getDatasetMetadata.data !== prevMetadataData) { + setPrevMetadataData(getDatasetMetadata.data); + const dataset = getDatasetMetadata.data?.datasets?.[0]; + if (dataset) { + const updatedData = defaultValuesPrepFn(dataset); setFormData(updatedData); - formDataRef.current = updatedData; setPreviousFormData(updatedData); } - }, [getDatasetMetadata.data]); + const promptMeta = dataset?.promptMetadata; + if (promptMeta) { + setPromptMetadataState({ + taskType: promptMeta.task_type || undefined, + domain: promptMeta.domain || undefined, + targetLanguages: promptMeta.target_languages || [], + targetModelTypes: promptMeta.target_model_types || [], + }); + } + } - const handleChange = (field: string, value: any) => { + useEffect(() => { + formDataRef.current = formData; + }, [formData]); + + const handleChange = (field: string, value: FormFieldValue) => { formDataRef.current = { ...formDataRef.current, [field]: value, @@ -582,8 +616,10 @@ export function EditMetadata({ id }: { id: string }) { }); }; - const getUpdateInput = (updatedData: any): UpdateMetadataInput | null => { - const changedFields: any = {}; + const getUpdateInput = ( + updatedData: MetadataFormData + ): UpdateMetadataInput | null => { + const changedFields: Record = {}; for (const key in updatedData) { const newValue = updatedData[key]; @@ -591,8 +627,8 @@ export function EditMetadata({ id }: { id: string }) { const isArray = Array.isArray(newValue); - const normalize = (val: any) => - isArray ? val?.map((item: any) => item?.value || item) : val; + const normalize = (val: FormFieldValue) => + isArray && Array.isArray(val) ? val.map(optionValue) : val; const newNormalized = normalize(newValue); const prevNormalized = normalize(prevValue); @@ -608,17 +644,15 @@ export function EditMetadata({ id }: { id: string }) { if (Object.keys(changedFields).length === 0) return null; - const transformedValues = Object.keys(changedFields).reduce( - (acc: any, key) => { - acc[key] = Array.isArray(changedFields[key]) - ? changedFields[key] - .map((item: any) => item?.value || item) - .join(', ') - : changedFields[key]; - return acc; - }, - {} - ); + const transformedValues = Object.keys(changedFields).reduce< + Record + >((acc, key) => { + const field = changedFields[key]; + acc[key] = Array.isArray(field) + ? field.map(optionValue).join(', ') + : String(field ?? ''); + return acc; + }, {}); return { dataset: id, @@ -638,28 +672,31 @@ export function EditMetadata({ id }: { id: string }) { id: key, value: transformedValues[key], })), - ...(changedFields.license && { license: changedFields.license }), - ...(changedFields.accessType && { - accessType: changedFields.accessType, + ...(typeof changedFields.license === 'string' && { + license: changedFields.license as UpdateMetadataInput['license'], + }), + ...(typeof changedFields.accessType === 'string' && { + accessType: + changedFields.accessType as UpdateMetadataInput['accessType'], }), ...(changedFields.description !== undefined && { - description: changedFields.description, + description: String(changedFields.description), }), ...(changedFields.tags && { - tags: changedFields.tags.map((item: any) => item.label), + tags: asOptionItems(changedFields.tags).map((item) => item.label), }), ...(changedFields.sectors && { - sectors: changedFields.sectors.map((item: any) => item.value), + sectors: asOptionItems(changedFields.sectors).map((item) => item.value), }), ...(changedFields.geographies && { - geographies: changedFields.geographies.map((item: any) => + geographies: asOptionItems(changedFields.geographies).map((item) => parseInt(item.value, 10) ), }), }; }; - const handleSave = (updatedData: any) => { + const handleSave = (updatedData: MetadataFormData) => { const updateInput = getUpdateInput(updatedData); if (!updateInput) return; @@ -669,7 +706,7 @@ export function EditMetadata({ id }: { id: string }) { const { setStatus, registerBeforeNavigateHandler } = useDatasetEditStatus(); useEffect(() => { - const handleSaveAsync = async (updatedData: any) => { + const handleSaveAsync = async (updatedData: MetadataFormData) => { const updateInput = getUpdateInput(updatedData); if (!updateInput) return; @@ -687,14 +724,18 @@ export function EditMetadata({ id }: { id: string }) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [registerBeforeNavigateHandler, updateMetadataMutation]); - function renderInputField(metadataFormItem: any) { + function formValueAsString(value: FormFieldValue): string { + return typeof value === 'string' ? value : ''; + } + + function renderInputField(metadataFormItem: MetadataFormItem) { if (metadataFormItem.dataType === 'STRING') { return (
handleChange(metadataFormItem.id, e)} onBlur={() => handleSave(formData)} // Save on blur /> @@ -707,7 +748,7 @@ export function EditMetadata({ id }: { id: string }) {
({ + list={(metadataFormItem.options || []).map((option) => ({ label: option, value: option, }))} @@ -715,7 +756,10 @@ export function EditMetadata({ id }: { id: string }) { displaySelected onChange={(value) => { handleChange(metadataFormItem.id, value); - handleSave({ ...formData, [metadataFormItem.id]: value }); // Save on change + handleSave({ + ...formData, + [metadataFormItem.id]: value, + }); }} />
@@ -730,7 +774,7 @@ export function EditMetadata({ id }: { id: string }) { ({ + ...((metadataFormItem.options || []).map((option) => ({ label: option, value: option, })) || []), @@ -740,7 +784,10 @@ export function EditMetadata({ id }: { id: string }) { selectedValue={prefillData} onChange={(value) => { handleChange(metadataFormItem.id, value); - handleSave({ ...formData, [metadataFormItem.id]: value }); // Save on change + handleSave({ + ...formData, + [metadataFormItem.id]: value, + }); }} />
@@ -752,7 +799,7 @@ export function EditMetadata({ id }: { id: string }) { { + list={ + getSectorsList.data?.sectors?.map((item) => { return { label: item.name, value: item.id }; - } - )} + }) || [] + } name="sectors" onChange={(value) => { - handleChange('sectors', value); - handleSave({ ...formData, sectors: value }); // Save on change + const next = Array.isArray(value) ? value : []; + handleChange('sectors', next); + handleSave({ ...formData, sectors: next }); }} /> ({ - label: item.value, - value: item.id, - }))} + list={ + getTagsList.data?.tags?.map((item) => ({ + label: item.value, + value: item.id, + })) || [] + } key={`tags-${getTagsList.data?.tags?.length}`} // forces remount on change label="Tags" requiredIndicator creatable onChange={(value) => { setIsTagsListUpdated(true); - handleChange('tags', value); - handleSave({ ...formData, tags: value }); + const next = Array.isArray(value) ? value : []; + handleChange('tags', next); + handleSave({ ...formData, tags: next }); }} /> ({ + getGeographiesList?.data?.geographies?.map((item) => ({ label: `${item.name}${item.parentId ? ` (${item.parentId.name})` : ''}`, value: item.id, })) || [] } selectedValue={formData.geographies} onChange={(value) => { - handleChange('geographies', value); - handleSave({ ...formData, geographies: value }); + const next = Array.isArray(value) ? value : []; + handleChange('geographies', next); + handleSave({ ...formData, geographies: next }); }} />
{getMetaDataListQuery?.data?.metadata - ?.filter( - (item: TypeMetadata) => item.dataType === 'MULTISELECT' - ) - .map((item: TypeMetadata) => ( + ?.filter((item) => item.dataType === 'MULTISELECT') + .map((item) => (
{renderInputField(item)}
))}
{getMetaDataListQuery?.data?.metadata - ?.filter( - (item: TypeMetadata) => item.dataType !== 'MULTISELECT' - ) - .map((item: TypeMetadata) => renderInputField(item))} + ?.filter((item) => item.dataType !== 'MULTISELECT') + .map((item) => renderInputField(item))}
@@ -928,10 +976,7 @@ export function EditMetadata({ id }: { id: string }) { displaySelected list={ getPromptTaskTypeEnum.data?.__type?.enumValues?.map( - (enumValue: { - name: string; - description?: string; - }) => ({ + (enumValue) => ({ label: enumValue.name .replace(/_/g, ' ') .replace(/\b\w/g, (c: string) => c.toUpperCase()), @@ -966,10 +1011,7 @@ export function EditMetadata({ id }: { id: string }) { displaySelected list={ getPromptDomainEnum.data?.__type?.enumValues?.map( - (enumValue: { - name: string; - description?: string; - }) => ({ + (enumValue) => ({ label: enumValue.name .replace(/_/g, ' ') .replace(/\b\w/g, (c: string) => c.toUpperCase()), @@ -1005,10 +1047,7 @@ export function EditMetadata({ id }: { id: string }) { creatable list={ getTargetLanguageEnum.data?.__type?.enumValues?.map( - (enumValue: { - name: string; - description?: string; - }) => ({ + (enumValue) => ({ label: enumValue.name .replace(/_/g, ' ') .replace(/\b\w/g, (c: string) => c.toUpperCase()), @@ -1028,7 +1067,7 @@ export function EditMetadata({ id }: { id: string }) { } onChange={(value) => { const languages = Array.isArray(value) - ? value.map((v: any) => v.value) + ? value.map((v) => v.value) : []; savePromptMetadata({ targetLanguages: languages }); }} @@ -1040,10 +1079,7 @@ export function EditMetadata({ id }: { id: string }) { creatable list={ getTargetModelTypeEnum.data?.__type?.enumValues?.map( - (enumValue: { - name: string; - description?: string; - }) => ({ + (enumValue) => ({ label: enumValue.name .replace(/_/g, ' ') .replace(/\b\w/g, (c: string) => c.toUpperCase()), @@ -1063,7 +1099,7 @@ export function EditMetadata({ id }: { id: string }) { } onChange={(value) => { const models = Array.isArray(value) - ? value.map((v: any) => v.value) + ? value.map((v) => v.value) : []; savePromptMetadata({ targetModelTypes: models }); }} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/ResourceSelector.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/ResourceSelector.tsx index 94fbce2d..6661cd38 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/ResourceSelector.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/components/ResourceSelector.tsx @@ -1,16 +1,72 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Button, Checkbox, Combobox, Icon, Text, TextField } from 'opub-ui'; import { cn } from '@/lib/utils'; import { Icons } from '@/components/icons'; import styles from '../edit.module.scss'; +interface SchemaField { + id: string | number; + fieldName: string; +} + +interface SelectedResource { + id: string; + name: string; + schema: SchemaField[]; +} + +interface AccessModelResource { + resource: string; + fields: number[]; +} + +interface AccessModelData { + dataset: string; + name: string; + description: string; + type: string; + resources: AccessModelResource[]; + accessModelId: string; +} + +interface OptionItem { + label: string; + value: string; +} + interface ResourceSelectorProps { - selectedResource: any; + selectedResource: SelectedResource; handleRemoveResource: (resourceId: string) => void; - accessModelData: any; - setAccessModelData: (data: any) => void; - handleSave: (updatedData: any) => void; + accessModelData: AccessModelData; + setAccessModelData: (data: AccessModelData) => void; + handleSave: (updatedData: AccessModelData) => void; +} + +function schemaToOptions(schema: SchemaField[]): OptionItem[] { + return schema.map((field) => ({ + label: field.fieldName, + value: field.id.toString(), + })); +} + +function fieldsToOptions( + fieldIds: number[], + schema: SchemaField[] +): OptionItem[] { + return fieldIds.flatMap((fieldId) => { + const field = schema.find( + (schemaField) => schemaField.id.toString() === fieldId.toString() + ); + return field + ? [ + { + label: field.fieldName, + value: field.id.toString(), + }, + ] + : []; + }); } const ResourceSelector: React.FC = ({ @@ -21,71 +77,43 @@ const ResourceSelector: React.FC = ({ handleSave, }) => { const [selectAllFields, setSelectAllFields] = useState(true); - const [options, setOptions] = useState<{ label: string; value: string }[]>( - [] + const [options, setOptions] = useState([]); + const [selectedFields, setSelectedFields] = useState([]); + const [prevSelectedResource, setPrevSelectedResource] = + useState(selectedResource); + const [prevAccessModelResources, setPrevAccessModelResources] = useState( + accessModelData.resources ); - const [selectedFields, setSelectedFields] = useState< - { label: string; value: string }[] - >([]); - - useEffect(() => { - const initialOptions = selectedResource.schema.map((field: any) => ({ - label: field.fieldName, - value: field.id.toString(), // Ensure ID is a string for Combobox - })); + if ( + selectedResource !== prevSelectedResource || + accessModelData.resources !== prevAccessModelResources + ) { + setPrevSelectedResource(selectedResource); + setPrevAccessModelResources(accessModelData.resources); + + const initialOptions = schemaToOptions(selectedResource.schema); setOptions(initialOptions); const selectedResourceData = accessModelData.resources.find( - (resource: any) => resource.resource === selectedResource.id + (resource) => resource.resource === selectedResource.id ); if (selectedResourceData) { - const initialSelectedFields = selectedResourceData.fields - .map((fieldId: any) => { - const field = selectedResource.schema.find( - (f: any) => f.id.toString() === fieldId.toString() - ); - return field - ? { - label: field.fieldName, - value: field.id.toString(), // Ensure ID is a string for Combobox - } - : null; - }) - .filter((field: any) => field !== null); // Filter out null values + const initialSelectedFields = fieldsToOptions( + selectedResourceData.fields, + selectedResource.schema + ); setSelectedFields(initialSelectedFields); setSelectAllFields( initialSelectedFields.length === initialOptions.length ); } else if (selectAllFields) { setSelectedFields(initialOptions); - const updatedData = { - ...accessModelData, - resources: [ - ...accessModelData.resources.filter( - (resource: any) => resource.resource !== selectedResource.id - ), - { - resource: selectedResource.id, - fields: initialOptions.map((option: any) => - parseInt(option.value, 10) - ), // Convert to integer - }, - ], - }; - setAccessModelData(updatedData); - handleSave(updatedData); } - }, [ - selectedResource, - accessModelData, - selectAllFields, - setAccessModelData, - handleSave, - ]); - - const handleFieldSelection = (selectedOptions: any) => { - const updatedFields = selectedOptions.map((option: any) => ({ + } + + const handleFieldSelection = (selectedOptions: OptionItem[]) => { + const updatedFields = selectedOptions.map((option) => ({ label: option.label, value: option.value, })); @@ -96,11 +124,11 @@ const ResourceSelector: React.FC = ({ ...accessModelData, resources: [ ...accessModelData.resources.filter( - (resource: any) => resource.resource !== selectedResource.id + (resource) => resource.resource !== selectedResource.id ), { resource: selectedResource.id, - fields: updatedFields.map((field: any) => parseInt(field.value, 10)), // Convert to integer + fields: updatedFields.map((field) => parseInt(field.value, 10)), }, ], }; @@ -120,11 +148,11 @@ const ResourceSelector: React.FC = ({ ...accessModelData, resources: [ ...accessModelData.resources.filter( - (resource: any) => resource.resource !== selectedResource.id + (resource) => resource.resource !== selectedResource.id ), { resource: selectedResource.id, - fields: updatedFields.map((field: any) => parseInt(field.value, 10)), // Convert to integer + fields: updatedFields.map((field) => parseInt(field.value, 10)), }, ], }; @@ -158,7 +186,11 @@ const ResourceSelector: React.FC = ({ selectedValue={selectedFields} name="" helpText="Use the dropdown to add specific fields" - onChange={(e: any) => handleFieldSelection(e)} + onChange={(value) => { + if (Array.isArray(value)) { + handleFieldSelection(value); + } + }} />
@@ -180,7 +212,7 @@ const ResourceSelector: React.FC = ({ console.log(e)} + onChange={(checked) => console.log(checked)} > Select All diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx index 0f51464f..d9a468b4 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/[id]/edit/publish/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { graphql } from '@/gql'; +import { DatasetsSummaryQuery } from '@/gql/generated/graphql'; import { useMutation, useQuery } from '@tanstack/react-query'; import { Accordion, @@ -25,7 +26,7 @@ import { formatDate, getWebsiteTitle, toTitleCase } from '@/lib/utils'; import { Icons } from '@/components/icons'; import { RichTextRenderer } from '@/components/RichTextRenderer'; -const datasetSummaryQuery: any = graphql(` +const datasetSummaryQuery = graphql(` query datasetsSummary($filters: DatasetFilter) { datasets(filters: $filters) { metadata { @@ -68,7 +69,7 @@ const datasetSummaryQuery: any = graphql(` } `); -const publishDatasetMutation: any = graphql(` +const publishDatasetMutation = graphql(` mutation publishDataset($datasetId: UUID!) { publishDataset(datasetId: $datasetId) { ... on TypeDataset { @@ -79,7 +80,60 @@ const publishDatasetMutation: any = graphql(` } `); -const generateColumnData = (name: any) => { +interface SchemaField { + fieldName: string; + description?: string | null; + format?: string | null; +} + +interface AccessModelResource { + resource: { + name: string; + type?: string | null; + }; +} + +interface ResourceSummary { + name: string; + type: string; + schema?: SchemaField[] | null; + modelResources?: AccessModelResource[]; +} + +interface PromptMetadata { + task_type?: string; + domain?: string; + target_languages?: string[]; + prompt_format?: string; + target_model_types?: string[]; + has_system_prompt?: boolean; + has_example_responses?: boolean; +} + +type DatasetSummaryResult = DatasetsSummaryQuery['datasets'][number]; + +type AccessModelSummary = { + id?: string; + name: string; + type: string; + modelResources?: AccessModelResource[]; +}; + +function hasAccessModels( + dataset: object +): dataset is { accessModels: AccessModelSummary[] } { + return 'accessModels' in dataset && Array.isArray(dataset.accessModels); +} + +function isPromptMetadata(value: unknown): value is PromptMetadata { + return typeof value === 'object' && value !== null; +} + +interface DialogTableRow { + dialog: AccessModelResource[] | SchemaField[]; +} + +const generateColumnData = (name: string) => { return [ { accessorKey: 'name', @@ -92,7 +146,7 @@ const generateColumnData = (name: any) => { { accessorKey: 'dialog', header: `${name === 'Access Type' ? 'Resources' : 'Fields'}`, - cell: ({ row }: any) => { + cell: ({ row }: { row: { original: DialogTableRow } }) => { return ( <> @@ -120,10 +174,15 @@ const generateColumnData = (name: any) => { header: 'Permissions', }, ]} - rows={row.original.dialog.map((item: any) => ({ - name: item.resource.name, - type: item.resource.type, - }))} + rows={row.original.dialog.map((item) => { + if ('resource' in item) { + return { + name: item.resource.name, + type: item.resource.type, + }; + } + return { name: '', type: '' }; + })} hideFooter /> ) : ( @@ -142,11 +201,16 @@ const generateColumnData = (name: any) => { header: 'Format', }, ]} - rows={row.original.dialog.map((item: any) => ({ - name: item.fieldName, - description: item.description, - format: item.format, - }))} + rows={row.original.dialog.map((item) => { + if ('fieldName' in item) { + return { + name: item.fieldName, + description: item.description, + format: item.format, + }; + } + return { name: '', description: '', format: '' }; + })} hideFooter /> )} @@ -159,15 +223,18 @@ const generateColumnData = (name: any) => { ]; }; -const generateTableData = (name: any, data: any) => { - return data.map((item: any) => ({ - name: item.name, - type: - name === 'Access Type' - ? toTitleCase(item.type.split('.').pop().toLowerCase()) - : item.type, - dialog: name === 'Access Type' ? item.modelResources : item.schema, - })); +const generateTableData = (name: string, data: ResourceSummary[]) => { + return data.map((item) => { + const permission = item.type.split('.').pop(); + return { + name: item.name, + type: + name === 'Access Type' + ? toTitleCase((permission ?? item.type).toLowerCase()) + : item.type, + dialog: (name === 'Access Type' ? item.modelResources : item.schema) ?? [], + }; + }); }; const Page = () => { @@ -177,8 +244,7 @@ const Page = () => { id: string; }>(); - const getDatasetsSummary: { data: any; isLoading: any; refetch: any } = - useQuery([`summary_${params.id}`], () => + const getDatasetsSummary = useQuery([`summary_${params.id}`], () => GraphQL( datasetSummaryQuery, { @@ -192,17 +258,21 @@ const Page = () => { getDatasetsSummary.refetch(); }); - const isPromptDataset = - getDatasetsSummary.data?.datasets[0]?.datasetType === 'PROMPT'; - const promptMetadata = getDatasetsSummary.data?.datasets[0]?.promptMetadata; + const dataset = getDatasetsSummary.data?.datasets[0]; + const isPromptDataset = dataset?.datasetType === 'PROMPT'; + const promptMetadata = isPromptMetadata(dataset?.promptMetadata) + ? dataset.promptMetadata + : null; + const accessModels = + dataset && hasAccessModels(dataset) ? dataset.accessModels : undefined; const Summary = [ { + kind: 'resources' as const, name: isPromptDataset ? 'Prompt Files' : 'Resource', - data: getDatasetsSummary.data?.datasets[0]?.resources, + data: dataset?.resources, error: - getDatasetsSummary.data && - getDatasetsSummary.data?.datasets[0]?.resources.length === 0 + getDatasetsSummary.data && (dataset?.resources.length ?? 0) === 0 ? isPromptDataset ? 'No Prompt Files found. Please add to continue.' : 'No Resources found. Please add to continue.' @@ -212,11 +282,11 @@ const Page = () => { ...(process.env.NEXT_PUBLIC_ENABLE_ACCESSMODEL === 'true' ? [ { + kind: 'access' as const, name: 'Access Type', - data: getDatasetsSummary.data?.datasets[0]?.accessModels, + data: accessModels, error: - getDatasetsSummary.data && - getDatasetsSummary.data?.datasets[0]?.accessModels.length === 0 + getDatasetsSummary.data && (accessModels?.length ?? 0) === 0 ? 'No Access Type found. Please add to continue.' : '', errorType: 'critical', @@ -224,12 +294,13 @@ const Page = () => { ] : []), { + kind: 'metadata' as const, name: 'Metadata', - data: getDatasetsSummary.data?.datasets[0]?.metadata, + data: dataset?.metadata, error: - getDatasetsSummary.data?.datasets[0]?.sectors.length === 0 || - getDatasetsSummary.data?.datasets[0]?.sectors.length === 0 || - getDatasetsSummary.data?.datasets[0]?.description.length === 0 + (dataset?.sectors.length ?? 0) === 0 || + (dataset?.tags.length ?? 0) === 0 || + (dataset?.description?.length ?? 0) === 0 ? 'Tags or Description or Sectors is missing. Please add to continue.' : '', errorType: 'critical', @@ -237,6 +308,7 @@ const Page = () => { ...(isPromptDataset ? [ { + kind: 'prompt' as const, name: 'Prompt Metadata', data: promptMetadata, error: '', @@ -249,19 +321,19 @@ const Page = () => { const PrimaryMetadata = [ { label: 'Dataset Name', - value: getDatasetsSummary.data?.datasets[0].title, + value: dataset?.title, }, { label: 'Description', - value: getDatasetsSummary.data?.datasets[0].description, + value: dataset?.description, }, { label: 'Date of Creation', - value: formatDate(getDatasetsSummary.data?.datasets[0].created) || '', + value: formatDate(dataset?.created ?? null) || '', }, { label: 'Date of Last Update', - value: formatDate(getDatasetsSummary.data?.datasets[0].modified) || '', + value: formatDate(dataset?.modified ?? null) || '', }, ]; const router = useRouter(); @@ -286,9 +358,13 @@ const Page = () => { `/dashboard/${params.entityType}/${params.entitySlug}/dataset` ); }, - onError: (err: any) => { + onError: (err: unknown) => { const errorMessage = - typeof err?.message === 'string' && err.message.trim() + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' && + err.message.trim() ? err.message.trim() : 'Unable to publish dataset right now. Please try again.'; toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID }); @@ -296,23 +372,24 @@ const Page = () => { } ); - const isPublishDisabled = (dataset: any) => { - if (!dataset) return true; + const isPublishDisabled = (current?: DatasetSummaryResult | null) => { + if (!current) return true; - const hasResources = dataset.resources.length > 0; - const hasAccessModels = dataset.accessModels?.length > 0; + const hasResources = current.resources.length > 0; + const hasAccessModelsFlag = + hasAccessModels(current) && (current.accessModels?.length ?? 0) > 0; const isAccessModelEnabled = process.env.NEXT_PUBLIC_ENABLE_ACCESSMODEL === 'true'; const hasRequiredMetadata = - dataset.sectors.length > 0 && - dataset.description.length > 0 && - dataset.tags.length > 0; + current.sectors.length > 0 && + (current.description?.length ?? 0) > 0 && + current.tags.length > 0; // No resources if (!hasResources) return true; // Access model check if enabled - if (isAccessModelEnabled && !hasAccessModels) return true; + if (isAccessModelEnabled && !hasAccessModelsFlag) return true; // Required metadata check return !hasRequiredMetadata; @@ -323,8 +400,8 @@ const Page = () => { useEffect(() => { const fetchTitle = async () => { try { - const urlItem = getDatasetsSummary.data?.datasets[0]?.metadata.find( - (item: any) => item.metadataItem?.dataType === 'URL' + const urlItem = dataset?.metadata.find( + (item) => item.metadataItem?.dataType === 'URL' ); if (urlItem && urlItem.value) { @@ -337,7 +414,7 @@ const Page = () => { }; fetchTitle(); - }, [getDatasetsSummary.data?.datasets, getDatasetsSummary.isLoading]); + }, [dataset?.metadata, getDatasetsSummary.data?.datasets, getDatasetsSummary.isLoading]); return ( <> @@ -391,7 +468,7 @@ const Page = () => { }} >
- {item.name === 'Prompt Metadata' ? ( + {item.kind === 'prompt' ? (
{item.data?.task_type && (
@@ -417,13 +494,13 @@ const Page = () => {
)} - {item.data?.target_languages?.length > 0 && ( + {(item.data?.target_languages?.length ?? 0) > 0 && (
Target Languages:
- {item.data.target_languages.map( + {item.data?.target_languages?.map( (lang: string, idx: number) => ( {lang} ) @@ -441,13 +518,13 @@ const Page = () => {
)} - {item.data?.target_model_types?.length > 0 && ( + {(item.data?.target_model_types?.length ?? 0) > 0 && (
Target Model Types:
- {item.data.target_model_types.map( + {item.data?.target_model_types?.map( (model: string, idx: number) => ( {model @@ -480,9 +557,9 @@ const Page = () => {
- ) : item.name !== 'Metadata' ? ( + ) : item.kind !== 'metadata' ? ( item.data && - item?.data.length > 0 && ( + item.data.length > 0 && (
{ ) )} - {item?.data?.map((item: any, index: any) => ( + {item.data?.map((metadataItem, index) => (
- {toTitleCase(item.metadataItem.label)}: + {toTitleCase(metadataItem.metadataItem.label)}: - {item.metadataItem.dataType !== 'URL' ? ( + {metadataItem.metadataItem.dataType !== 'URL' ? ( {' '} - {item.value === '' ? 'NA' : item.value} + {metadataItem.value === '' + ? 'NA' + : metadataItem.value} ) : ( - + { Sectors:
- {getDatasetsSummary.data?.datasets[0]?.sectors?.map( - (item: any, index: any) => ( - {item.name} + {dataset?.sectors?.map((sector, index) => ( + {sector.name} ) )}
@@ -559,9 +640,8 @@ const Page = () => { Tags:
- {getDatasetsSummary.data?.datasets[0].tags.map( - (item: any, index: any) => ( - {item.value} + {dataset?.tags.map((tag, index) => ( + {tag.value} ) )}
@@ -575,9 +655,7 @@ const Page = () => { ))} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/new/components/new-dataset.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/new/components/new-dataset.tsx index a0d40837..686497c2 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/new/components/new-dataset.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/new/components/new-dataset.tsx @@ -27,26 +27,35 @@ const defaultValBase: Props = { terms: false, }; +interface DatasetMutationPayload { + dataset_data: { + title: string; + description: string; + dataset_type?: string; + id?: string; + }; +} + +interface CreateDatasetProps { + defaultVal?: PatchDataset; + submitRef: React.RefObject; + isLoading?: boolean; + mutate?: (res: DatasetMutationPayload) => void; + mutatePatch: (res: DatasetMutationPayload) => void; +} + export function CreateDataset({ defaultVal, submitRef, isLoading, mutate, mutatePatch, -}: { - defaultVal?: PatchDataset; - submitRef: React.RefObject; - isLoading?: boolean; - // mutate?: (res: { dataset_data: CreateDatasetInput }) => void; - // mutatePatch?: (res: { dataset_data: PatchDatasetInput }) => void; - mutate?: any; - mutatePatch: any; -}) { +}: CreateDatasetProps) { const defaultValue = defaultVal || defaultValBase; return ( { + onSubmit={(value) => { if (mutatePatch && defaultVal) { mutatePatch({ dataset_data: { diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page-layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page-layout.tsx index 133e7ae8..eeb11a4a 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page-layout.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page-layout.tsx @@ -9,7 +9,7 @@ import { GraphQL } from '@/lib/api'; import { ActionBar } from './components/action-bar'; import { Content } from './components/content'; -const createDatasetMutationDoc: any = graphql(` +const createDatasetMutationDoc = graphql(` mutation GenerateDatasetname { addDataset { success @@ -43,9 +43,9 @@ export const Page = () => { const queryClient = useQueryClient(); const { mutate, isLoading } = useMutation( - () => GraphQL(createDatasetMutationDoc, ownerArgs || {}, []), + () => GraphQL(createDatasetMutationDoc, ownerArgs || {}), { - onSuccess: (data: any) => { + onSuccess: (data) => { if (data.addDataset.success) { toast('Dataset created successfully!'); if (isValidParams && entityType) { @@ -58,7 +58,10 @@ export const Page = () => { ); } } else { - toast('Error: ' + data.addDataset.errors.fieldErrors[0].messages[0]); + const errorMessage = + data.addDataset.errors?.fieldErrors?.[0]?.messages[0] ?? + 'Unable to create dataset'; + toast('Error: ' + errorMessage); } }, } diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page.tsx index 61bfe64c..f80a4dd6 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/dataset/page.tsx @@ -1,6 +1,11 @@ 'use client'; import { graphql } from '@/gql'; +import { + DatasetStatus, + DatasetType as GqlDatasetType, + Ordering, +} from '@/gql/generated/graphql'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import { parseAsString, useQueryState } from 'nuqs'; @@ -18,7 +23,7 @@ import { Content } from './components/content'; import { DatasetType, DatasetTypeModal } from './components/dataset-type-modal'; import { Navigation } from './components/navigate-org-datasets'; -const allDatasetsQueryDoc: any = graphql(` +const allDatasetsQueryDoc = graphql(` query allDatasetsQuery($filters: DatasetFilter, $order: DatasetOrder) { datasets(filters: $filters, order: $order) { title @@ -30,7 +35,7 @@ const allDatasetsQueryDoc: any = graphql(` } `); -const createDatasetMutationDoc: any = graphql(` +const createDatasetMutationDoc = graphql(` mutation GenerateDatasetName($createInput: CreateDatasetInput) { addDataset(createInput: $createInput) { success @@ -49,13 +54,13 @@ const createDatasetMutationDoc: any = graphql(` } `); -const deleteDatasetMutationDoc: any = graphql(` +const deleteDatasetMutationDoc = graphql(` mutation deleteDatasetMutation($datasetId: UUID!) { deleteDataset(datasetId: $datasetId) } `); -const unPublishDataset: any = graphql(` +const unPublishDataset = graphql(` mutation unPublishDatasetMutation($datasetId: UUID!) { unPublishDataset(datasetId: $datasetId) { __typename @@ -85,8 +90,7 @@ export default function DatasetPage() { const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString); const [isTypeModalOpen, setIsTypeModalOpen] = useState(false); - const AllDatasetsQuery: { data: any; isLoading: boolean; refetch: any } = - useQuery( + const AllDatasetsQuery = useQuery( [ `fetch_datasets_org_dashboard`, entityType, @@ -96,9 +100,12 @@ export default function DatasetPage() { () => GraphQL(allDatasetsQueryDoc, ownerArgs || {}, { filters: { - status: navigationTab === 'published' ? 'PUBLISHED' : 'DRAFT', + status: + navigationTab === 'published' + ? DatasetStatus.Published + : DatasetStatus.Draft, }, - order: { modified: 'DESC' }, + order: { modified: Ordering.Desc }, }), { enabled: isValidParams } ); @@ -111,11 +118,7 @@ export default function DatasetPage() { } }, [navigationTab, isValidParams, AllDatasetsQuery, setNavigationTab]); - const DeleteDatasetMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const DeleteDatasetMutation = useMutation( [`delete_dataset`], (data: { datasetId: string }) => GraphQL(deleteDatasetMutationDoc, ownerArgs || {}, { @@ -128,19 +131,25 @@ export default function DatasetPage() { AllDatasetsQuery.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); + onError: (err: unknown) => { + const message = + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' + ? err.message.split(':')[0] + : 'Unknown error'; + toast('Error: ' + message); }, } ); - const CreateDatasetMutation: { mutate: any; isLoading: boolean; error: any } = - useMutation( - (datasetType: DatasetType) => + const CreateDatasetMutation = useMutation( + (datasetType: GqlDatasetType) => GraphQL(createDatasetMutationDoc, ownerArgs || {}, { createInput: { datasetType }, }), { - onSuccess: (data: any) => { + onSuccess: (data) => { setIsTypeModalOpen(false); if (data.addDataset.success) { toast('Dataset created successfully!'); @@ -152,16 +161,15 @@ export default function DatasetPage() { ); } } else { - toast('Error: ' + data.addDataset.errors.fieldErrors[0].messages[0]); + const errorMessage = + data.addDataset.errors?.fieldErrors?.[0]?.messages[0] ?? + 'Unable to create dataset'; + toast('Error: ' + errorMessage); } }, } ); - const UnpublishDatasetMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const UnpublishDatasetMutation = useMutation( [`unpublish_dataset`], (data: { datasetId: string }) => GraphQL(unPublishDataset, ownerArgs || {}, { datasetId: data.datasetId }), @@ -172,8 +180,15 @@ export default function DatasetPage() { AllDatasetsQuery.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); + onError: (err: unknown) => { + const message = + typeof err === 'object' && + err !== null && + 'message' in err && + typeof err.message === 'string' + ? err.message.split(':')[0] + : 'Unknown error'; + toast('Error: ' + message); }, } ); @@ -182,7 +197,7 @@ export default function DatasetPage() { return null; } - let navigationOptions = [ + const navigationOptions = [ { label: 'Drafts', url: `drafts`, @@ -195,11 +210,23 @@ export default function DatasetPage() { }, ]; + interface DatasetTableRow { + title: string; + id: string; + datasetType?: string | null; + created: string; + modified: string; + } + + interface DatasetTableCell { + row: { original: DatasetTableRow }; + } + const datasetsListColumns = [ { accessorKey: 'title', header: 'Title', - cell: ({ row }: any) => + cell: ({ row }: DatasetTableCell) => navigationTab === 'published' ? ( {row.original.title} ) : ( @@ -215,7 +242,7 @@ export default function DatasetPage() { { accessorKey: 'datasetType', header: 'Type', - cell: ({ row }: any) => ( + cell: ({ row }: DatasetTableCell) => ( {row.original.datasetType === 'PROMPT' ? 'Prompt' : 'Data'} ), }, @@ -224,7 +251,7 @@ export default function DatasetPage() { { accessorKey: 'delete', header: 'Delete', - cell: ({ row }: any) => + cell: ({ row }: DatasetTableCell) => navigationTab === 'published' ? (
); diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Contributors.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Contributors.tsx index 8fced4f5..295347f0 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Contributors.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Contributors.tsx @@ -1,51 +1,85 @@ import Image from 'next/image'; import { Text } from 'opub-ui'; -const Contributors = ({ data }: { data: any }) => { - const ContributorDetails = [ +interface Contributor { + fullName?: string | null; + profilePicture?: { url?: string | null } | null; +} + +interface Organization { + name?: string | null; + logo?: { url?: string | null } | null; +} + +interface UseCaseContributorsData { + useCases: Array<{ + contributors?: Contributor[] | null; + supportingOrganizations?: Organization[] | null; + partnerOrganizations?: Organization[] | null; + } | null> | null; +} + +interface ContributorsProps { + data?: UseCaseContributorsData | null; +} + +interface ContributorSection { + label: string; + value: string; + image: Contributor[]; +} + +interface OrgSection { + label: string; + value: string; + image: Organization[]; +} + +const Contributors = ({ data }: ContributorsProps) => { + const ContributorDetails: ContributorSection[] = [ { label: 'Contributors', value: - data?.useCases[0]?.contributors.length > 0 - ? data?.useCases[0]?.contributors - .map((item: any) => item.fullName) - .join(', ') + (data?.useCases?.[0]?.contributors?.length ?? 0) > 0 + ? data?.useCases?.[0]?.contributors + ?.map((item) => item.fullName) + .join(', ') || 'No Contributors' : 'No Contributors', - image: data?.useCases[0]?.contributors, + image: data?.useCases?.[0]?.contributors ?? [], }, ]; - const OrgDetails = [ + const OrgDetails: OrgSection[] = [ { label: 'Supporters', value: - data?.useCases[0]?.supportingOrganizations.length > 0 - ? data?.useCases[0]?.supportingOrganizations - .map((item: any) => item.name) - .join(', ') + (data?.useCases?.[0]?.supportingOrganizations?.length ?? 0) > 0 + ? data?.useCases?.[0]?.supportingOrganizations + ?.map((item) => item.name) + .join(', ') || 'No Supporting Organizations' : 'No Supporting Organizations', - image: data?.useCases[0]?.supportingOrganizations, + image: data?.useCases?.[0]?.supportingOrganizations ?? [], }, { label: 'Partners', value: - data?.useCases[0]?.partnerOrganizations.length > 0 - ? data?.useCases[0]?.partnerOrganizations - .map((item: any) => item.name) - .join(', ') + (data?.useCases?.[0]?.partnerOrganizations?.length ?? 0) > 0 + ? data?.useCases?.[0]?.partnerOrganizations + ?.map((item) => item.name) + .join(', ') || 'No Partner Organizations' : 'No Partner Organizations', - image: data?.useCases[0]?.partnerOrganizations, + image: data?.useCases?.[0]?.partnerOrganizations ?? [], }, ]; return (
- {ContributorDetails.map((item: any, index: number) => ( + {ContributorDetails.map((item, index) => (
{item.label}:
- {item?.image.map((data: any, index: number) => ( + {item?.image.map((data, index) => (
{
))} - {OrgDetails.map((item: any, index: number) => ( + {OrgDetails.map((item, index) => (
{item.label}:
- {item.image.map((data: any, index: number) => ( + {item.image.map((data, index) => (
{ ); }; -export default Contributors; \ No newline at end of file +export default Contributors; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Dashboards.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Dashboards.tsx index 56e3457e..a9f35369 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Dashboards.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Dashboards.tsx @@ -1,12 +1,21 @@ import { Table, Text } from 'opub-ui'; -const Dashboards = ({ data }: { data: any }) => { +interface DashboardItem { + name: string; + link: string; +} + +interface DashboardsProps { + data: DashboardItem[] | null | undefined; +} + +const Dashboards = ({ data }: DashboardsProps) => { const dashboardColumns = [ { accessorKey: 'name', header: 'Name' }, { accessorKey: 'link', header: 'Link' }, ]; - const generatePublisherData = (list: Array) => { + const generatePublisherData = (list: DashboardItem[] | null | undefined) => { return list?.map((item) => { return { name: item.name, @@ -16,10 +25,10 @@ const Dashboards = ({ data }: { data: any }) => { }; return (
- {data?.length > 0 ? ( + {data && data.length > 0 ? (
) : ( diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx index 5c91b2d7..0e0e653e 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/[id]/publish/Details.tsx @@ -6,17 +6,57 @@ import { Text } from 'opub-ui'; import { getWebsiteTitle } from '@/lib/utils'; import { RichTextRenderer } from '@/components/RichTextRenderer'; -const Details = ({ data }: { data: any }) => { - const [platformTitle, setPlatformTitle] = useState(null); +interface NamedItem { + id?: string | null; + name?: string | null; + code?: string | null; + value?: string | null; +} + +interface UseCasePublishDetails { + title?: string | null; + summary?: string | null; + runningStatus?: string | null; + startedOn?: string | null; + completedOn?: string | null; + platformUrl?: string | { value?: string } | null; + sectors?: Array<{ name?: string | null } | null> | null; + geographies?: NamedItem[] | null; + sdgs?: NamedItem[] | null; + tags?: Array<{ value?: string | null } | null> | null; + metadata?: Array<{ + value?: string | null; + metadataItem?: { label?: string | null } | null; + }> | null; + logo?: { path?: string | null } | null; +} + +interface DetailsProps { + data?: { useCases?: Array | null } | null; +} + +const Details = ({ data }: DetailsProps) => { const useCase = data?.useCases?.[0]; const platformUrl = useCase?.platformUrl; + const [platformTitle, setPlatformTitle] = useState( + platformUrl === null ? 'N/A' : null + ); + const [prevPlatformUrl, setPrevPlatformUrl] = useState(platformUrl); + if (platformUrl !== prevPlatformUrl) { + setPrevPlatformUrl(platformUrl); + if (platformUrl === null) { + setPlatformTitle('N/A'); + } + } useEffect(() => { + if (!useCase || platformUrl === null) return; + const fetchTitle = async () => { try { const urlItem = useCase?.platformUrl; - if (urlItem && urlItem.value) { + if (urlItem && typeof urlItem === 'object' && urlItem.value) { const title = await getWebsiteTitle(urlItem.value); setPlatformTitle(title); } @@ -25,42 +65,43 @@ const Details = ({ data }: { data: any }) => { } }; - if (!useCase) return; - - if (platformUrl === null) { - setPlatformTitle('N/A'); - } else { - fetchTitle(); - } + fetchTitle(); }, [useCase, platformUrl]); + const platformHref = + typeof platformUrl === 'string' + ? platformUrl + : platformUrl && typeof platformUrl === 'object' + ? platformUrl.value + : undefined; + const PrimaryDetails = [ - { label: 'Use Case Name', value: data?.useCases[0]?.title }, - { label: 'Summary', value: data?.useCases[0]?.summary }, + { label: 'Use Case Name', value: useCase?.title }, + { label: 'Summary', value: useCase?.summary }, { label: 'Running Status', - value: data?.useCases[0]?.runningStatus, + value: useCase?.runningStatus, }, - { label: 'Started On', value: data?.useCases[0]?.startedOn }, + { label: 'Started On', value: useCase?.startedOn }, { label: 'Completed On', - value: data?.useCases[0]?.completedOn, + value: useCase?.completedOn, }, - { label: 'Sector', value: data?.useCases[0]?.sectors[0]?.name }, + { label: 'Sector', value: useCase?.sectors?.[0]?.name }, { label: 'Geography', - value: data?.useCases[0]?.geographies - ?.map((geo: any) => geo.name) + value: useCase?.geographies + ?.map((geo) => geo?.name) .join(', '), }, { label: 'SDG Goals', - value: data?.useCases[0]?.sdgs - ?.map((sdg: any) => `${sdg.code} - ${sdg.name}`) + value: useCase?.sdgs + ?.map((sdg) => `${sdg?.code} - ${sdg?.name}`) .join(', '), }, - { label: 'Tags', value: data?.useCases[0]?.tags[0]?.value }, - ...(data?.useCases[0]?.metadata?.map((meta: any) => ({ + { label: 'Tags', value: useCase?.tags?.[0]?.value }, + ...(useCase?.metadata?.map((meta) => ({ label: meta.metadataItem?.label, value: meta.value, })) || []), @@ -90,10 +131,10 @@ const Details = ({ data }: { data: any }) => { Platform URL:
- {data.useCases[0].platformUrl ? ( + {platformHref ? ( {
- {data?.useCases[0]?.logo && ( + {useCase?.logo && (
@@ -117,8 +158,8 @@ const Details = ({ data }: { data: any }) => {
{ entitySlug: string; id: string; }>(); - const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery( + const UseCaseData = useQuery( [`fetch_UsecaseDetails`, params.id, params.entityType, params.entitySlug], () => GraphQL( @@ -168,9 +168,9 @@ const Publish = () => { `/dashboard/${params.entityType}/${params.entitySlug}/usecases` ); }, - onError: (err: any) => { + onError: (err: unknown) => { const errorMessage = - typeof err?.message === 'string' && err.message.trim() + typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' && err.message.trim() ? err.message.trim() : 'Unable to publish use case right now. Please try again.'; toast(`Error: ${errorMessage}`, { id: PUBLISH_ERROR_TOAST_ID }); @@ -183,45 +183,58 @@ const Publish = () => { name: 'Details', data: UseCaseData.data?.useCases, error: - UseCaseData.data?.useCases[0]?.sectors.length === 0 || - UseCaseData.data?.useCases[0]?.summary.length === 0 || - UseCaseData.data?.useCases[0]?.sdgs.length === 0 || - UseCaseData.data?.useCases[0]?.logo === null || - !UseCaseData.data?.useCases[0]?.startedOn + UseCaseData.data?.useCases?.[0]?.sectors?.length === 0 || + UseCaseData.data?.useCases?.[0]?.summary?.length === 0 || + UseCaseData.data?.useCases?.[0]?.sdgs?.length === 0 || + UseCaseData.data?.useCases?.[0]?.logo === null || + !UseCaseData.data?.useCases?.[0]?.startedOn ? 'Summary, SDG, Sectors, Logo, or Started On is missing. Please add to continue.' : '', errorType: 'critical', }, { name: 'Assign', - data: UseCaseData?.data?.useCases[0]?.datasets, + data: UseCaseData?.data?.useCases?.[0]?.datasets, error: - UseCaseData.data && UseCaseData.data?.useCases[0]?.datasets.length === 0 + UseCaseData.data && UseCaseData.data?.useCases?.[0]?.datasets?.length === 0 ? 'No datasets assigned. Please assign to continue.' : '', }, { name: 'Dashboards', - data: UseCaseData?.data?.useCases[0]?.length > 0, + data: UseCaseData?.data?.useCases?.[0] != null && + 'length' in UseCaseData.data.useCases[0] && + typeof UseCaseData.data.useCases[0].length === 'number' && + UseCaseData.data.useCases[0].length > 0, error: '', }, { name: 'Contributors', - data: UseCaseData?.data?.useCases[0]?.length > 0, + data: UseCaseData?.data?.useCases?.[0] != null && + 'length' in UseCaseData.data.useCases[0] && + typeof UseCaseData.data.useCases[0].length === 'number' && + UseCaseData.data.useCases[0].length > 0, error: '', }, ]; - const isPublishDisabled = (useCase: any) => { + const isPublishDisabled = (useCase: { + datasets?: unknown[] | null; + sectors?: unknown[] | null; + summary?: string | null; + sdgs?: unknown[] | null; + logo?: unknown; + startedOn?: string | null; + } | null | undefined) => { if (!useCase) return true; - const hasDatasets = useCase?.datasets.length > 0; + const hasDatasets = (useCase.datasets?.length ?? 0) > 0; const hasRequiredMetadata = - useCase.sectors.length > 0 && - useCase?.summary.length > 0 && - useCase?.sdgs.length > 0 && - useCase?.logo !== null && - !!useCase?.startedOn; + (useCase.sectors?.length ?? 0) > 0 && + (useCase.summary?.length ?? 0) > 0 && + (useCase.sdgs?.length ?? 0) > 0 && + useCase.logo !== null && + !!useCase.startedOn; // No datasets assigned if (!hasDatasets) return true; diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx index 8ad48330..973d18e2 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/edit/layout.tsx @@ -11,7 +11,7 @@ import StepNavigation from '../../components/StepNavigation'; import TitleBar from '../../components/title-bar'; import { EditStatusProvider, useEditStatus } from './context'; -const UpdateUseCaseTitleMutation: any = graphql(` +const UpdateUseCaseTitleMutation = graphql(` mutation updateUseCaseTitle($data: UseCaseInputPartial!) { updateUseCase(data: $data) { __typename @@ -21,7 +21,7 @@ const UpdateUseCaseTitleMutation: any = graphql(` } `); -const FetchUseCaseTitle: any = graphql(` +const FetchUseCaseTitle = graphql(` query UseCaseTitle($filters: UseCaseFilter) { useCases(filters: $filters) { id @@ -41,8 +41,12 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => { const USECASE_TITLE_SUCCESS_TOAST_ID = 'usecase-title-save-success'; const USECASE_TITLE_ERROR_TOAST_ID = 'usecase-title-save-error'; const queryClient = useQueryClient(); - const getErrorMessage = (error: any, fallback: string) => - typeof error?.message === 'string' && error.message.trim() + const getErrorMessage = (error: unknown, fallback: string) => + typeof error === 'object' && + error !== null && + 'message' in error && + typeof error.message === 'string' && + error.message.trim() ? error.message.trim() : fallback; @@ -58,7 +62,7 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => { return pathName.indexOf(v) >= 0; }); - const UseCaseData: { data: any; isLoading: boolean; refetch: any } = useQuery( + const UseCaseData = useQuery( [`fetch_UseCaseData`], () => GraphQL( @@ -99,7 +103,7 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => { ], }); }, - onError: (error: any) => { + onError: (error: unknown) => { toast( `Error: ${getErrorMessage(error, 'Unable to update use case title right now. Please try again.')}`, { id: USECASE_TITLE_ERROR_TOAST_ID } @@ -149,7 +153,7 @@ const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
mutate({ data: { title: e, id: params.id.toString() } })} loading={editMutationLoading} diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/page.tsx index 750cc743..5a5285c0 100644 --- a/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/page.tsx +++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/usecases/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { graphql } from '@/gql'; +import { Ordering, UseCaseStatus } from '@/gql/generated/graphql'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import { parseAsString, useQueryState } from 'nuqs'; @@ -16,7 +17,21 @@ import { formatDate } from '@/lib/utils'; import { ActionBar } from '../dataset/components/action-bar'; import { Navigation } from '../dataset/components/navigate-org-datasets'; -const allUseCases: any = graphql(` +interface UseCaseListItem { + id: string; + title?: string | null; + created?: string | null; + modified?: string | null; +} + +interface UseCaseTableRow { + id: string; + title?: string | null; + created: string; + modified: string; +} + +const allUseCases = graphql(` query UseCasesData($filters: UseCaseFilter, $order: UseCaseOrder) { useCases(filters: $filters, order: $order) { title @@ -27,13 +42,13 @@ const allUseCases: any = graphql(` } `); -const deleteUseCase: any = graphql(` +const deleteUseCase = graphql(` mutation deleteUseCase($useCaseId: String!) { deleteUseCase(useCaseId: $useCaseId) } `); -const AddUseCase: any = graphql(` +const AddUseCase = graphql(` mutation Addusecase { addUseCase { __typename @@ -45,7 +60,7 @@ const AddUseCase: any = graphql(` } `); -const unPublishUseCase: any = graphql(` +const unPublishUseCase = graphql(` mutation unPublishUseCaseMutation($useCaseId: String!) { unpublishUseCase(useCaseId: $useCaseId) { __typename @@ -73,7 +88,7 @@ export default function DatasetPage() { const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString); - const AllUseCases: { data: any; isLoading: boolean; refetch: any } = useQuery( + const AllUseCases = useQuery( [`fetch_UseCases`, entityType, entitySlug, navigationTab ?? 'drafts'], () => GraphQL( @@ -81,9 +96,9 @@ export default function DatasetPage() { ownerArgs || {}, { filters: { - status: navigationTab === 'published' ? 'PUBLISHED' : 'DRAFT', + status: navigationTab === 'published' ? UseCaseStatus.Published : UseCaseStatus.Draft, }, - order: { modified: 'DESC' }, + order: { modified: Ordering.Desc }, } ), { enabled: isValidParams } @@ -97,11 +112,7 @@ export default function DatasetPage() { } }, [navigationTab, isValidParams, setNavigationTab, AllUseCases]); - const DeleteUseCaseMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const DeleteUseCaseMutation = useMutation( [`delete_Usecase`], (data: { id: string }) => GraphQL( @@ -116,39 +127,33 @@ export default function DatasetPage() { AllUseCases.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0]); }, } ); - const CreateUseCase: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const CreateUseCase = useMutation( [`delete_Usecase`], - () => GraphQL(AddUseCase, ownerArgs || {}, []), + () => GraphQL(AddUseCase, ownerArgs || {}), { - onSuccess: (response: any) => { + onSuccess: (response) => { toast(`UseCase created successfully`); if (isValidParams && entityType && entitySlug) { + const created = response.addUseCase; + const createdId = 'id' in created ? created.id : undefined; router.push( - `/dashboard/${entityType}/${entitySlug}/usecases/edit/${response.addUseCase.id}/details` + `/dashboard/${entityType}/${entitySlug}/usecases/edit/${createdId}/details` ); AllUseCases.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0]); }, } ); - const UnpublishDatasetMutation: { - mutate: any; - isLoading: boolean; - error: any; - } = useMutation( + const UnpublishDatasetMutation = useMutation( [`unpublish_usecase`], (data: { id: string }) => GraphQL( @@ -163,8 +168,8 @@ export default function DatasetPage() { AllUseCases.refetch(); } }, - onError: (err: any) => { - toast('Error: ' + err.message.split(':')[0]); + onError: (err: unknown) => { + toast('Error: ' + (typeof err === 'object' && err !== null && 'message' in err && typeof err.message === 'string' ? err.message : String(err)).split(':')[0]); }, } ); @@ -173,7 +178,7 @@ export default function DatasetPage() { return null; } - let navigationOptions = [ + const navigationOptions = [ { label: 'Drafts', url: `drafts`, @@ -189,11 +194,11 @@ export default function DatasetPage() { { accessorKey: 'title', header: 'Title', - cell: ({ row }: any) => + cell: ({ row }: { row: { original: UseCaseTableRow } }) => navigationTab === 'published' ? ( {row.original.title} @@ -214,7 +219,7 @@ export default function DatasetPage() { { accessorKey: 'delete', header: 'Delete', - cell: ({ row }: any) => + cell: ({ row }: { row: { original: UseCaseTableRow } }) => navigationTab === 'published' ? (