Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useQuery } from "@tanstack/react-query";
import DataRecordsPanel from "@/app/(private)/map/[id]/components/InspectorPanel/DataRecordsPanel";
import { useInspectorDataSourceConfig } from "@/app/(private)/map/[id]/hooks/useInspectorDataSourceConfig";
import { useDataSources } from "@/hooks/useDataSources";
import { useTRPC } from "@/services/trpc/react";
import { cn } from "@/shadcn/utils";

Expand All @@ -16,6 +17,8 @@ export function DefaultInspectorPreview({
const trpc = useTRPC();

const inspectorConfig = useInspectorDataSourceConfig(dataSourceId);
const { getDataSourceById } = useDataSources();
const dataSource = getDataSourceById(dataSourceId);

const selectedCount =
inspectorConfig?.items.filter((i) => i.type === "column").length ?? 0;
Expand Down Expand Up @@ -58,6 +61,11 @@ export function DefaultInspectorPreview({
/>
)}
</div>
<div className="shrink-0 px-3 py-2 border-t border-neutral-200">
<p className="text-xs text-muted-foreground">
Organisation: {dataSource?.organisationName ?? "—"}
</p>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ export default function DataSourceConfigPage() {
<h1 className="text-3xl font-medium tracking-tight mt-3 min-w-0">
{dataSource.name}
</h1>
<p className="text-sm text-muted-foreground mt-1">
Organisation: {dataSource.organisationName ?? "—"}
</p>
<p className="text-sm text-muted-foreground mt-1">
Configure default inspector settings for this public data source.
</p>
Expand Down
2 changes: 2 additions & 0 deletions src/app/(private)/(dashboards)/superadmin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ export default function SuperadminPage() {
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Organisation</TableHead>
<TableHead>Record type</TableHead>
<TableHead>Records</TableHead>
<TableHead>Inspector config</TableHead>
Expand All @@ -331,6 +332,7 @@ export default function SuperadminPage() {
publicDataSources?.map((ds) => (
<TableRow key={ds.id}>
<TableCell className="font-medium">{ds.name}</TableCell>
<TableCell>{ds.organisationName ?? "-"}</TableCell>
<TableCell>{ds.recordType}</TableCell>
<TableCell>{ds.recordCount.toLocaleString()}</TableCell>
<TableCell>
Expand Down
18 changes: 16 additions & 2 deletions src/app/(private)/hooks/useDataSourceListCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,29 @@ export function useDataSourceListCache() {
(old: DataSourceByOrganisation[] | undefined) =>
old?.map((ds) =>
ds.id === dataSourceId
? { ...ds, ...updater({ ...ds, organisationOverride: null }) }
? {
...ds,
...updater({
...ds,
organisationName: "",
organisationOverride: null,
}),
}
: ds,
Comment on lines 58 to 69
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In updateDataSource, the wrapper passed to updater() overwrites organisationName with an empty string before calling the updater. If the updater returns { ...ds, ... } (common pattern), this will clear an existing organisation name in the cache until the next refetch. Preserve the existing organisationName when present (only default it when missing).

Copilot uses AI. Check for mistakes.
),
);
queryClient.setQueriesData(
{ queryKey: trpc.dataSource.byId.queryKey() },
(old: DataSourceById | undefined) => {
if (!old || old.id !== dataSourceId) return old;
return { ...old, ...updater({ ...old, organisationOverride: null }) };
return {
...old,
...updater({
...old,
organisationName: "",
organisationOverride: null,
}),
};
Comment on lines 73 to +83
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue for the byId cache: organisationName is set to "" before calling updater(), which can wipe the cached organisation name if the updater spreads its input. Pass through the existing value (or default only when absent) instead of always forcing an empty string.

Copilot uses AI. Check for mistakes.
},
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Settings } from "lucide-react";
import { BookOpen, Settings } from "lucide-react";
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BookOpenIcon is imported but never used in this component. Remove the unused import to avoid lint/TS unused-import errors.

Suggested change
import { BookOpen, Settings } from "lucide-react";
import { Settings } from "lucide-react";

Copilot uses AI. Check for mistakes.
import TogglePanel from "@/app/(private)/map/[id]/components/TogglePanel";
import DataSourceIcon from "@/components/DataSourceIcon";
import IconButtonWithTooltip from "@/components/IconButtonWithTooltip";
Expand Down Expand Up @@ -102,6 +102,16 @@ export default function DataRecordsPanel({
))}
</ul>
)}

{dataSource?.organisationName && (
<div className="flex items-center gap-1 pt-2 border-t border-neutral-200/70 text-muted-foreground">
<BookOpen className="w-3 h-3 shrink-0" />
<p className="text-[11px] truncate">
Published by <span className="text-neutral-400">•</span>{" "}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This UI shows "Published by" based solely on dataSource.organisationName. If dataSource.public is false (private/internal data sources in the inspector), this copy becomes misleading compared to the existing "Published" badge semantics elsewhere. Consider gating this on dataSource.public or adjusting the copy to not imply publication.

Suggested change
Published by <span className="text-neutral-400"></span>{" "}
{dataSource.public ? "Published by" : "Provided by"}{" "}
<span className="text-neutral-400"></span>{" "}

Copilot uses AI. Check for mistakes.
{dataSource.organisationName}
</p>
</div>
)}
</div>
</TogglePanel>
);
Expand Down
50 changes: 45 additions & 5 deletions src/components/DataSourceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ function LastImportedOrDateAddedMeta({
return null;
}

function OrganisationMeta({
organisationName,
compact,
}: {
organisationName: string | null | undefined;
compact?: boolean;
}) {
const name = organisationName?.trim();
if (!name) return null;
return (
<span
className={cn(
"inline-flex items-center gap-1 whitespace-nowrap text-muted-foreground",
compact ? "text-[11px]" : "text-xs",
)}
>
Published by
<span className="text-neutral-400">•</span>
<span className="truncate">{name}</span>
</span>
);
}
Comment on lines +69 to +90
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrganisationMeta always renders the text "Published by" whenever organisationName is set, but it isn’t gated on dataSource.public. This will label private/internal data sources as "Published" in contexts like the organisation’s own data source list where the "Published" badge is not shown. Consider rendering this meta only when dataSource.public is true, or change the wording to something that doesn’t imply the public flag.

Copilot uses AI. Check for mistakes.

export interface DataSourceItemProps {
dataSource: DataSourceWithImportInfo;
className?: string;
Expand Down Expand Up @@ -235,15 +258,23 @@ export function DataSourceItem({
</p>
)}

{(lastImportedText || dataSource.createdAt) && (
<div className="flex items-center gap-4">
<div className="mt-1">
<LastImportedOrDateAddedMeta
lastImportedText={lastImportedText}
createdAt={dataSource.createdAt}
<OrganisationMeta
organisationName={dataSource.organisationName}
compact
/>
</div>
)}
{(lastImportedText || dataSource.createdAt) && (
<div className="mt-1">
<LastImportedOrDateAddedMeta
lastImportedText={lastImportedText}
createdAt={dataSource.createdAt}
compact
/>
</div>
)}
</div>

{columnPills.length > 0 && columnPreviewVariant === "pills" && (
<div className="flex flex-wrap gap-1.5 mt-2">
Expand Down Expand Up @@ -405,6 +436,13 @@ export function DataSourceItem({
)}
</div>
) : null}

<div className="col-span-2 pt-1">
<OrganisationMeta
organisationName={dataSource.organisationName}
compact
/>
</div>
</div>
</div>
);
Expand Down Expand Up @@ -463,6 +501,8 @@ export function DataSourceItem({
</span>
)}
</div>

<OrganisationMeta organisationName={dataSource.organisationName} />
</div>

{showColumnPreview && columnPreviewVariant === "text" ? (
Expand Down
8 changes: 0 additions & 8 deletions src/server/repositories/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,6 @@ export async function updateDefaultChoroplethConfig(
.execute();
}

export async function findPublicDataSources() {
return await db
.selectFrom("dataSource")
.where("public", "=", true)
.selectAll()
.execute();
}

export async function updateColumnDefsWithEnrichment(
dataSourceId: string,
enrichedColumnDefs: ColumnDef[],
Expand Down
56 changes: 49 additions & 7 deletions src/server/trpc/routers/dataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
deleteDataSource,
findDataSourceById,
findDataSourcesByIds,
findPublicDataSources,
getJobInfo,
getUniqueColumnValues,
updateDataSource,
Expand Down Expand Up @@ -57,15 +56,25 @@ import {
router,
superadminProcedure,
} from "../index";
import type { DataSource } from "@/models/DataSource";
import type { DataSourceEvent } from "@/server/events";
import type { DataSourceUpdate } from "@/server/models/DataSource";

export const dataSourceRouter = router({
listPublic: superadminProcedure.query(async () => {
const dataSources = await findPublicDataSources();
const dataSources = await db
.selectFrom("dataSource")
.leftJoin("organisation", "dataSource.organisationId", "organisation.id")
.where("dataSource.public", "=", true)
.selectAll("dataSource")
.select(["organisation.name as organisationName"])
.execute();

const withImportInfo = await addImportInfo(dataSources);
Comment on lines +65 to 72
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listPublic no longer calls findPublicDataSources(), so the findPublicDataSources import at the top of this file appears to be unused and may fail lint/TS builds with unused-import checks. Remove the unused import or switch back to using the repository helper.

Copilot uses AI. Check for mistakes.
return withImportInfo.map((ds) => ({ ...ds, organisationOverride: null }));
return withImportInfo.map((ds) => ({
...ds,
organisationName: ds.organisationName ?? "",
organisationOverride: null,
}));
}),
updateDefaultInspectorConfig: superadminProcedure
.input(
Expand Down Expand Up @@ -100,7 +109,26 @@ export const dataSourceRouter = router({
const ids = getVisualisedDataSourceIds(map.config, view);
if (!ids.length) return [];
const dataSources = await findDataSourcesByIds(ids);
const withImportInfo = await addImportInfo(dataSources);
const organisationIds = [
...new Set(dataSources.map((ds) => ds.organisationId).filter(Boolean)),
];
const organisations =
organisationIds.length > 0
? await db
.selectFrom("organisation")
.where("id", "in", organisationIds)
.select(["id", "name"])
.execute()
: [];
const organisationNameById = new Map(
organisations.map((o) => [o.id, o.name ?? null]),
);
const withOrg = dataSources.map((ds) => ({
...ds,
organisationName: organisationNameById.get(ds.organisationId) ?? "",
}));

const withImportInfo = await addImportInfo(withOrg);
return withImportInfo.map((ds) => ({
...ds,
organisationOverride: null,
Expand Down Expand Up @@ -137,6 +165,7 @@ export const dataSourceRouter = router({
return eb.or(filter);
})
.selectAll("dataSource")
.select(["organisation.name as organisationName"])
.execute();

const orgId = input?.activeOrganisationId;
Expand All @@ -162,6 +191,7 @@ export const dataSourceRouter = router({
const withImportInfo = await addImportInfo(filteredDataSources);
return withImportInfo.map((ds) => ({
...ds,
organisationName: ds.organisationName ?? "",
organisationOverride: overrideMap.get(ds.id) ?? null,
}));
}),
Expand All @@ -172,9 +202,20 @@ export const dataSourceRouter = router({
.selectAll("dataSource")
.execute();

return addImportInfo(dataSources);
const withOrg = dataSources.map((ds) => ({
...ds,
organisationName: ctx.organisation.name ?? "",
}));

return addImportInfo(withOrg);
}),
byId: dataSourceOwnerProcedure.query(async ({ ctx }) => {
const organisation = await db
.selectFrom("organisation")
.where("id", "=", ctx.dataSource.organisationId)
.select(["name"])
.executeTakeFirst();

const recordCount = await db
.selectFrom("dataRecord")
.where("dataSourceId", "=", ctx.dataSource.id)
Expand All @@ -196,6 +237,7 @@ export const dataSourceRouter = router({
]);
return {
...ctx.dataSource,
organisationName: organisation?.name ?? "",
config: {
...ctx.dataSource.config,
__SERIALIZE_CREDENTIALS: true,
Expand Down Expand Up @@ -760,7 +802,7 @@ export const dataSourceRouter = router({
}),
});

const addImportInfo = async (dataSources: DataSource[]) => {
const addImportInfo = async <T extends { id: string }>(dataSources: T[]) => {
// Get import info for all data sources
const importInfos = await Promise.all(
dataSources.map((dataSource) =>
Expand Down
Loading