diff --git a/plugins/functions/d2e-webapi/src/api/WebApiConceptSetAPI.ts b/plugins/functions/d2e-webapi/src/api/WebApiConceptSetAPI.ts index ba82b1c10b..c4ab43fceb 100644 --- a/plugins/functions/d2e-webapi/src/api/WebApiConceptSetAPI.ts +++ b/plugins/functions/d2e-webapi/src/api/WebApiConceptSetAPI.ts @@ -1,3 +1,5 @@ +import { ConceptSetNameConflictError } from "../errors/ConceptSetErrors.ts"; + const DEFAULT_WEBAPI_URL = "http://localhost:33001/WebAPI"; export interface IWebApiConceptSetHeader { @@ -52,16 +54,6 @@ export interface IWebApiConcept { const CONTROL_CHAR_REGEX = /[\x00-\x1F\x7F]/; -const assertNonNegativeInteger = (value: unknown, field: string): number => { - if ( - typeof value !== "number" || !Number.isInteger(value) || value < 0 || - value > Number.MAX_SAFE_INTEGER - ) { - throw new Error(`Invalid ${field}: expected non-negative integer`); - } - return value; -}; - const assertPositiveInteger = (value: unknown, field: string): number => { if ( typeof value !== "number" || !Number.isInteger(value) || value <= 0 || @@ -224,6 +216,11 @@ export class WebApiConceptSetAPI { }); if (!response.ok) { + // WebAPI has no duplicate-name check. The `uq_cs_name` constraint on + // `webapi.concept_set` rejects the insert and surfaces as a 409. + if (response.status === 409) { + throw new ConceptSetNameConflictError(payload.name); + } throw new Error( `Failed to create WebAPI concept set: ${response.status}`, ); @@ -259,6 +256,11 @@ export class WebApiConceptSetAPI { ); if (!response.ok) { + // A rename onto a name another concept set already holds trips the same + // `uq_cs_name` constraint as a create. + if (response.status === 409) { + throw new ConceptSetNameConflictError(payload.name); + } throw new Error( `Failed to update WebAPI concept set ${validatedId}: ${response.status}`, ); @@ -318,25 +320,4 @@ export class WebApiConceptSetAPI { ); } } - - async checkIfConceptSetExists(id: number, name: string): Promise { - const validatedId = assertNonNegativeInteger(id, "id"); - const validatedName = assertName(name); - - const url = buildUrl(this.baseUrl, "conceptset", validatedId, "exists"); - url.searchParams.set("name", validatedName); - - const response = await fetch(url, { - method: "GET", - headers: buildHeaders(this.token), - }); - - if (!response.ok) { - throw new Error( - `Failed to check WebAPI concept set existence for ${validatedId}: ${response.status}`, - ); - } - - return response.json(); - } } diff --git a/plugins/functions/d2e-webapi/src/dto/conceptset.ts b/plugins/functions/d2e-webapi/src/dto/conceptset.ts index 8fee5fcf11..52da5c9a25 100644 --- a/plugins/functions/d2e-webapi/src/dto/conceptset.ts +++ b/plugins/functions/d2e-webapi/src/dto/conceptset.ts @@ -157,6 +157,15 @@ export const ConceptSetInUseErrorDto = z.object({ }); export type IConceptSetInUseErrorDto = z.infer; +export const ConceptSetNameConflictErrorDto = z.object({ + error: z.literal("CONCEPT_SET_NAME_EXISTS"), + message: z.string(), + conceptSetName: z.string(), +}); +export type IConceptSetNameConflictErrorDto = z.infer< + typeof ConceptSetNameConflictErrorDto +>; + export const IncludedConceptDto = z.object({ CONCEPT_ID: z.number(), CONCEPT_NAME: z.string(), diff --git a/plugins/functions/d2e-webapi/src/errors/ConceptSetErrors.ts b/plugins/functions/d2e-webapi/src/errors/ConceptSetErrors.ts index b4091ce206..ac86eb5d3f 100644 --- a/plugins/functions/d2e-webapi/src/errors/ConceptSetErrors.ts +++ b/plugins/functions/d2e-webapi/src/errors/ConceptSetErrors.ts @@ -18,6 +18,20 @@ export class ConceptSetInUseError extends Error { } } +/** + * Thrown when WebAPI rejects a concept set name that is already taken. + * WebAPI has no duplicate-name check of its own; the `uq_cs_name` unique + * constraint on `webapi.concept_set` rejects the write and WebAPI reports it + * as HTTP 409. Route handlers map this error back to a 409 so that the browser + * can show a duplicate-name message instead of a generic failure. + */ +export class ConceptSetNameConflictError extends Error { + constructor(public readonly conceptSetName: string) { + super(`A concept set named "${conceptSetName}" already exists`); + this.name = "ConceptSetNameConflictError"; + } +} + /** * Thrown when concept set validation fails (e.g., invalid ID format). */ diff --git a/plugins/functions/d2e-webapi/src/routes/conceptset.ts b/plugins/functions/d2e-webapi/src/routes/conceptset.ts index 23db4d9b20..2a7a1c295d 100644 --- a/plugins/functions/d2e-webapi/src/routes/conceptset.ts +++ b/plugins/functions/d2e-webapi/src/routes/conceptset.ts @@ -9,11 +9,15 @@ import { ConceptSetItemsResponseDto, ConceptSetCreateDto, ConceptSetInUseErrorDto, + ConceptSetNameConflictErrorDto, IConceptSetCheckResponseDto, IncludedConceptsRequestDto, IncludedConceptsResponseDto, } from "../dto/conceptset.ts"; -import { ConceptSetInUseError } from "../errors/ConceptSetErrors.ts"; +import { + ConceptSetInUseError, + ConceptSetNameConflictError, +} from "../errors/ConceptSetErrors.ts"; import { getConceptSet, @@ -55,10 +59,14 @@ export const conceptset: FastifyPluginAsyncZod = async function (app) { "/", { schema: { - description: "Save a new concept set to the database", + description: + "Save a new concept set to the database. Returns 409 if the name is already taken.", body: ConceptSetCreateDto, tags: ["conceptset"], - response: { 200: ConceptSetResponseDto }, + response: { + 200: ConceptSetResponseDto, + 409: ConceptSetNameConflictErrorDto, + }, security: [ { bearerAuth: [], @@ -68,12 +76,25 @@ export const conceptset: FastifyPluginAsyncZod = async function (app) { }, }, async (req, res) => { - const results = await createConceptSet( - req.token, - req.datasetId, - req.body - ); - res.send(results); + try { + const results = await createConceptSet( + req.token, + req.datasetId, + req.body + ); + res.send(results); + } catch (error) { + if (error instanceof ConceptSetNameConflictError) { + res.status(409).send({ + error: "CONCEPT_SET_NAME_EXISTS", + message: + `A concept set named "${error.conceptSetName}" already exists. Choose another name.`, + conceptSetName: error.conceptSetName, + }); + return; + } + throw error; + } } ); @@ -155,11 +176,15 @@ export const conceptset: FastifyPluginAsyncZod = async function (app) { "/:id", { schema: { - description: "Updates the concept set for the selected concept set.", + description: + "Updates the concept set for the selected concept set. Returns 409 if the name is already taken.", tags: ["conceptset"], params: z.object({ id: ConceptSetIdParamSchema }), body: ConceptSetCreateDto, - response: { 200: z.boolean() }, + response: { + 200: z.boolean(), + 409: ConceptSetNameConflictErrorDto, + }, security: [ { bearerAuth: [], @@ -170,13 +195,26 @@ export const conceptset: FastifyPluginAsyncZod = async function (app) { }, async (req, res) => { const { id } = req.params; - const results = await updateConceptSet( - req.token, - req.datasetId, - id, - req.body - ); - res.send(results); + try { + const results = await updateConceptSet( + req.token, + req.datasetId, + id, + req.body + ); + res.send(results); + } catch (error) { + if (error instanceof ConceptSetNameConflictError) { + res.status(409).send({ + error: "CONCEPT_SET_NAME_EXISTS", + message: + `A concept set named "${error.conceptSetName}" already exists. Choose another name.`, + conceptSetName: error.conceptSetName, + }); + return; + } + throw error; + } } ); @@ -185,7 +223,7 @@ export const conceptset: FastifyPluginAsyncZod = async function (app) { { schema: { description: - "Check if a concept set with the same name exists in the WebAPIdatabase. The name is checked against the selected concept set IDto ensure that only the selected concept set ID has the name specified.", + "Check whether a concept set with the same name exists in the legacy (terminology) store. This is the only store probed here; the WebAPI store enforces name uniqueness with the uq_cs_name constraint and reports a duplicate as HTTP 409. The selected concept set ID is excluded so that only that set may hold the name.", tags: ["conceptset"], params: z.object({ id: ConceptSetIdParamSchema }), querystring: z.object({ name: z.string() }), diff --git a/plugins/functions/d2e-webapi/src/services/conceptset.service.test.ts b/plugins/functions/d2e-webapi/src/services/conceptset.service.test.ts index cf3d13ff48..b4ad9610ae 100644 --- a/plugins/functions/d2e-webapi/src/services/conceptset.service.test.ts +++ b/plugins/functions/d2e-webapi/src/services/conceptset.service.test.ts @@ -35,7 +35,7 @@ const { mapWebApiConceptSetToFacadeConceptSet, } = await import("./conceptset.service.ts"); -const { ConceptSetExpressionError } = await import( +const { ConceptSetExpressionError, ConceptSetNameConflictError } = await import( "../errors/ConceptSetErrors.ts" ); const { WebApiConceptSetAPI } = await import("../api/WebApiConceptSetAPI.ts"); @@ -73,6 +73,54 @@ Deno.test("legacy concept sets remain writable in facade responses", () => { assertEquals(conceptSet.source, "legacy"); }); +Deno.test("legacy concept sets are writable only for their owner", () => { + const owned = mapLegacyConceptSetToWebApiConceptSet( + { + id: 21, + name: "Owned legacy set", + shared: false, + concepts: [], + userName: "owner-1", + createdBy: "owner-1", + modifiedBy: "owner-1", + createdDate: "2026-05-01T00:00:00.000Z", + modifiedDate: "2026-05-02T00:00:00.000Z", + }, + "owner-1", + ); + assertEquals(owned.hasWriteAccess, true); + + const sharedFromSomeoneElse = mapLegacyConceptSetToWebApiConceptSet( + { + id: 22, + name: "Shared legacy set", + shared: true, + concepts: [], + userName: "owner-1", + createdBy: "owner-1", + modifiedBy: "owner-1", + createdDate: "2026-05-01T00:00:00.000Z", + modifiedDate: "2026-05-02T00:00:00.000Z", + }, + "current-user", + ); + assertEquals(sharedFromSomeoneElse.hasWriteAccess, false); + + // Without a caller-provided user the historical writable default applies. + const noUser = mapLegacyConceptSetToWebApiConceptSet({ + id: 23, + name: "No user legacy set", + shared: true, + concepts: [], + userName: "owner-1", + createdBy: "owner-1", + modifiedBy: "owner-1", + createdDate: "2026-05-01T00:00:00.000Z", + modifiedDate: "2026-05-02T00:00:00.000Z", + }); + assertEquals(noUser.hasWriteAccess, true); +}); + Deno.test("native WebAPI concept sets are exposed with compound facade ids", () => { const conceptSet = mapWebApiConceptSetToFacadeConceptSet({ id: 42, @@ -846,56 +894,121 @@ Deno.test("getConceptSets propagates WebAPI errors instead of returning silent e } }); -Deno.test("checkIfConceptSetExists allows webapi exclude id 0 for new concept sets", async () => { +Deno.test("checkIfConceptSetExists checks the legacy store only", async () => { + // WebAPI is deliberately not asked. Its `uq_cs_name` constraint rejects a + // duplicate at write time, and asking it here needs a permission that the + // `concept set creator` role does not hold. const originalGetConceptSetsTerm = TerminologySvcAPI.prototype.getConceptSets; - const originalCheckIfConceptSetExists = - WebApiConceptSetAPI.prototype.checkIfConceptSetExists; try { - TerminologySvcAPI.prototype.getConceptSets = () => - Promise.resolve([] as unknown as ITerminologyConceptSet[]); - WebApiConceptSetAPI.prototype.checkIfConceptSetExists = ( - id: number, - name: string, - ) => { - assertEquals(id, 0); - assertEquals(name, "Name"); - return Promise.resolve(0); + let legacyCalls = 0; + TerminologySvcAPI.prototype.getConceptSets = () => { + legacyCalls += 1; + return Promise.resolve([] as unknown as ITerminologyConceptSet[]); }; const result = await checkIfConceptSetExists( "token", "dataset-1", - 0, + "webapi:7", "Name", ); + assertEquals(result, 0); + assertEquals(legacyCalls, 1); } finally { TerminologySvcAPI.prototype.getConceptSets = originalGetConceptSetsTerm; - WebApiConceptSetAPI.prototype.checkIfConceptSetExists = - originalCheckIfConceptSetExists; } }); -Deno.test("checkIfConceptSetExists propagates WebAPI errors instead of returning silent zero", async () => { +Deno.test("checkIfConceptSetExists reports a legacy concept set with the same name", async () => { const originalGetConceptSetsTerm = TerminologySvcAPI.prototype.getConceptSets; - const originalCheckIfConceptSetExists = - WebApiConceptSetAPI.prototype.checkIfConceptSetExists; try { TerminologySvcAPI.prototype.getConceptSets = () => - Promise.resolve([] as unknown as ITerminologyConceptSet[]); - WebApiConceptSetAPI.prototype.checkIfConceptSetExists = () => - Promise.reject(new Error("WebAPI unavailable")); + Promise.resolve( + [{ id: 3, name: "Name" }] as unknown as ITerminologyConceptSet[], + ); - await assertRejects( - () => checkIfConceptSetExists("token", "dataset-1", "webapi:7", "Name"), - Error, - "WebAPI unavailable", + assertEquals( + await checkIfConceptSetExists("token", "dataset-1", "webapi:7", "Name"), + 1, ); } finally { TerminologySvcAPI.prototype.getConceptSets = originalGetConceptSetsTerm; - WebApiConceptSetAPI.prototype.checkIfConceptSetExists = - originalCheckIfConceptSetExists; + } +}); + +Deno.test("checkIfConceptSetExists excludes the legacy row being renamed", async () => { + const originalGetConceptSetsTerm = TerminologySvcAPI.prototype.getConceptSets; + + try { + TerminologySvcAPI.prototype.getConceptSets = () => + Promise.resolve( + [{ id: 3, name: "Name" }] as unknown as ITerminologyConceptSet[], + ); + + // Renaming legacy:3 to the name it already holds is not a duplicate. + assertEquals( + await checkIfConceptSetExists("token", "dataset-1", "legacy:3", "Name"), + 0, + ); + } finally { + TerminologySvcAPI.prototype.getConceptSets = originalGetConceptSetsTerm; + } +}); + +Deno.test("WebApiConceptSetAPI.createConceptSet maps a 409 to a name conflict", async () => { + const originalFetch = globalThis.fetch; + + try { + globalThis.fetch = (() => + Promise.resolve(new Response(null, { status: 409 }))) as typeof fetch; + + const api = new WebApiConceptSetAPI("token"); + const error = await assertRejects( + () => api.createConceptSet({ name: "Taken" }), + ConceptSetNameConflictError, + ); + assertEquals(error.conceptSetName, "Taken"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +Deno.test("WebApiConceptSetAPI.updateConceptSet maps a 409 to a name conflict", async () => { + const originalFetch = globalThis.fetch; + + try { + globalThis.fetch = (() => + Promise.resolve(new Response(null, { status: 409 }))) as typeof fetch; + + const api = new WebApiConceptSetAPI("token"); + const error = await assertRejects( + () => api.updateConceptSet(7, { id: 7, name: "Taken" }), + ConceptSetNameConflictError, + ); + assertEquals(error.conceptSetName, "Taken"); + } finally { + globalThis.fetch = originalFetch; + } +}); + +Deno.test("WebApiConceptSetAPI.createConceptSet keeps other failures as plain errors", async () => { + const originalFetch = globalThis.fetch; + + try { + globalThis.fetch = (() => + Promise.resolve(new Response(null, { status: 500 }))) as typeof fetch; + + const api = new WebApiConceptSetAPI("token"); + const error = await assertRejects( + () => api.createConceptSet({ name: "Any" }), + Error, + "Failed to create WebAPI concept set: 500", + ); + assertEquals(error instanceof ConceptSetNameConflictError, false); + } finally { + globalThis.fetch = originalFetch; } }); diff --git a/plugins/functions/d2e-webapi/src/services/conceptset.service.ts b/plugins/functions/d2e-webapi/src/services/conceptset.service.ts index 13b68fa510..078125286f 100644 --- a/plugins/functions/d2e-webapi/src/services/conceptset.service.ts +++ b/plugins/functions/d2e-webapi/src/services/conceptset.service.ts @@ -95,6 +95,7 @@ export const getConceptSet = async ( conceptSetId: string | number, ): Promise => { const ref = parseConceptSetRef(conceptSetId); + const currentUserId = getCurrentUserId(token); if (ref.source === "webapi") { const webApiConceptSetApi = new WebApiConceptSetAPI(token); @@ -110,7 +111,10 @@ export const getConceptSet = async ( datasetId, ); - return mapLegacyConceptSetToWebApiConceptSet(terminologyConceptSet); + return mapLegacyConceptSetToWebApiConceptSet( + terminologyConceptSet, + currentUserId, + ); }; export const getConceptSets = async ( @@ -119,6 +123,7 @@ export const getConceptSets = async ( ): Promise => { const terminologySvcApi = new TerminologySvcAPI(token); const webApiConceptSetApi = new WebApiConceptSetAPI(token); + const currentUserId = getCurrentUserId(token); const [terminologyConceptSets, webApiConceptSets] = await Promise.all([ terminologySvcApi.getConceptSets(datasetId), @@ -126,7 +131,9 @@ export const getConceptSets = async ( ]); const merged = [ - ...terminologyConceptSets.map(mapLegacyConceptSetToWebApiConceptSet), + ...terminologyConceptSets.map((conceptSet) => + mapLegacyConceptSetToWebApiConceptSet(conceptSet, currentUserId), + ), ...webApiConceptSets.map(mapWebApiConceptSetToFacadeConceptSet), ]; @@ -437,21 +444,19 @@ export const checkIfConceptSetExists = async ( ): Promise => { const ref = parseConceptSetRef(conceptSetId); const terminologySvcApi = new TerminologySvcAPI(token); - const webApiConceptSetApi = new WebApiConceptSetAPI(token); - - // Probe WebAPI with the source-scoped externalId so that an in-flight - // rename of the same row doesn't false-positive against itself. For - // legacy refs there is no WebAPI counterpart to exclude, so use 0 (a - // never-existing id) which still surfaces unrelated WebAPI duplicates. - const webApiExcludeId = ref.source === "webapi" ? ref.externalId : 0; - const [terminologyConceptSets, webApiExistsCount] = await Promise.all([ - terminologySvcApi.getConceptSets(datasetId), - webApiConceptSetApi.checkIfConceptSetExists( - webApiExcludeId, - conceptSetName, - ), - ]); + // Only the legacy store is probed here. The WebAPI store enforces name + // uniqueness with the `uq_cs_name` constraint and reports a duplicate as + // HTTP 409 on create and on update, which the routes map to a typed error. + // + // Asking WebAPI the same question needs `read:conceptset` or + // `write:conceptset`. The `concept set creator` role holds neither, so a + // researcher-only user was denied here and could never save a concept set, + // even though the create itself was permitted. Atlas3 does not ask this + // question at all. + const terminologyConceptSets = await terminologySvcApi.getConceptSets( + datasetId, + ); // For legacy refs we must exclude the same legacy row by id; for webapi // refs the legacy table is a disjoint namespace, so no row should match @@ -464,7 +469,7 @@ export const checkIfConceptSetExists = async ( : terminologyConceptSet.name === conceptSetName ); - return (result === undefined ? 0 : 1) + webApiExistsCount; + return result === undefined ? 0 : 1; }; const parseDateValue = (value: string | number): number => { @@ -743,6 +748,7 @@ export const getIncludedConcepts = async ( export const mapLegacyConceptSetToWebApiConceptSet = ( conceptSet: ITerminologyConceptSet, + currentUserId?: string, ): IConceptSetResponseDto => { return { createdDate: Date.parse(conceptSet.createdDate), @@ -754,7 +760,13 @@ export const mapLegacyConceptSetToWebApiConceptSet = ( name: conceptSet.userName, }, tags: [], - hasWriteAccess: true, + // A legacy set is writable only by its owner. A shared set owned by another + // user reports no write access, so the UI does not offer an active Update + // button. When the caller passes no user (for example in unit tests), keep + // the historical writable default so behaviour is unchanged. + hasWriteAccess: currentUserId + ? conceptSet.createdBy === currentUserId + : true, hasReadAccess: true, id: formatConceptSetRef({ source: "legacy", externalId: conceptSet.id }), externalId: conceptSet.id, @@ -764,6 +776,21 @@ export const mapLegacyConceptSetToWebApiConceptSet = ( }; }; +const getCurrentUserId = (token: string): string | undefined => { + try { + const encoded = token.replace(/^bearer\s+/i, "").split(".")[1]; + if (!encoded) { + return undefined; + } + const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/"); + const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); + const payload = JSON.parse(atob(padded)) as { sub?: unknown }; + return typeof payload.sub === "string" ? payload.sub : undefined; + } catch { + return undefined; + } +}; + export const mapWebApiConceptSetToFacadeConceptSet = ( conceptSet: IWebApiConceptSetHeader, ): IConceptSetResponseDto => { diff --git a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSets.tsx b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSets.tsx index 72b4347975..3046262931 100644 --- a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSets.tsx +++ b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSets.tsx @@ -77,7 +77,7 @@ export const ConceptSets: FC = ({ isAtlas }) => { }, [fetchData]); const handleAddAndEditConceptSet = useCallback( - (conceptSetId?: string) => { + (conceptSetId?: string, canWrite?: boolean) => { if (!datasetId) return; const event = new CustomEvent<{ props: TerminologyProps }>( "alp-terminology-open", @@ -85,6 +85,7 @@ export const ConceptSets: FC = ({ isAtlas }) => { detail: { props: { selectedConceptSetId: conceptSetId, + selectedConceptSetCanWrite: canWrite ?? true, onClose: () => fetchData(), mode: "CONCEPT_SET", selectedDatasetId: datasetId, diff --git a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.test.tsx b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.test.tsx index 9862b401af..da882f25b2 100644 --- a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.test.tsx +++ b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.test.tsx @@ -89,7 +89,7 @@ describe("ConceptSetsTable", () => { fireEvent.click(buttons[0]); fireEvent.click(buttons[1]); - expect(onAddEdit).toHaveBeenCalledWith("legacy:15"); + expect(onAddEdit).toHaveBeenCalledWith("legacy:15", true); expect(onDelete).toHaveBeenCalledWith( expect.objectContaining({ id: "legacy:15", hasWriteAccess: true }) ); @@ -130,7 +130,7 @@ describe("ConceptSetsTable", () => { fireEvent.click(buttons[0]); fireEvent.click(buttons[1]); - expect(onAddEdit).toHaveBeenCalledWith("webapi:7"); + expect(onAddEdit).toHaveBeenCalledWith("webapi:7", true); expect(onDelete).toHaveBeenCalledWith( expect.objectContaining({ id: "webapi:7", hasWriteAccess: true }) ); @@ -233,7 +233,7 @@ describe("ConceptSetsTable", () => { fireEvent.click(buttons[0]); fireEvent.click(buttons[1]); - expect(onAddEdit).toHaveBeenCalledWith("webapi:2"); + expect(onAddEdit).toHaveBeenCalledWith("webapi:2", true); expect(onDelete).toHaveBeenCalledWith( expect.objectContaining({ id: "webapi:2", hasWriteAccess: false }) ); diff --git a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.tsx b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.tsx index bfdbe48fd6..387d01c632 100644 --- a/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.tsx +++ b/plugins/ui/apps/concept-sets/src/ConceptSets/ConceptSetsTable.tsx @@ -21,7 +21,7 @@ import "./ConceptSets.scss"; interface ConceptSetsTableProps { data: ConceptSet[]; isLoading: boolean; - onAddEdit: (conceptSetId?: string) => void; + onAddEdit: (conceptSetId?: string, canWrite?: boolean) => void; onDelete: (conceptSet: ConceptSet) => void; userName?: string; } @@ -124,7 +124,7 @@ export const ConceptSetsTable: FC = ({ ) } - onClick={() => onAddEdit(row.original.id)} + onClick={() => onAddEdit(row.original.id, isWritable)} /> {isWritable && ( void; selectedConceptSetId?: string; + selectedConceptSetCanWrite?: boolean; mode?: | "CONCEPT_MAPPING" | "CONCEPT_SET" @@ -210,18 +211,16 @@ const NameSection = ({ /> )} - {isUserConceptSet && ( -