Skip to content

Commit

Permalink
N21-1248 Configure External Tools in Boards (#2860)
Browse files Browse the repository at this point in the history
- add configuration dialog for tools in boards
  • Loading branch information
MarvinOehlerkingCap authored Oct 19, 2023
1 parent da6e4be commit 0341a18
Show file tree
Hide file tree
Showing 41 changed files with 1,593 additions and 359 deletions.
15 changes: 12 additions & 3 deletions src/components/data-board/BoardApi.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ContentElementType,
CreateCardBodyParamsRequiredEmptyElementsEnum,
CreateContentElementBodyParams,
ExternalToolElementContent,
ExternalToolElementResponse,
FileElementContent,
LinkElementContent,
RichTextElementContent,
Expand Down Expand Up @@ -92,9 +92,12 @@ export const useBoardApi = () => {
};
}

if (element.type === ContentElementType.ExternalTool) {
if (isExternalToolElement(element)) {
return {
content: element.content as ExternalToolElementContent,
content: {
contextExternalToolId:
element.content.contextExternalToolId ?? undefined,
},
type: ContentElementType.ExternalTool,
};
}
Expand All @@ -109,6 +112,12 @@ export const useBoardApi = () => {
throw new Error("element.type mapping is undefined for updateElementCall");
};

const isExternalToolElement = (
element: AnyContentElement
): element is ExternalToolElementResponse => {
return element.type === ContentElementType.ExternalTool;
};

const createElementCall = async (
cardId: string,
params: CreateContentElementBodyParams
Expand Down
7 changes: 5 additions & 2 deletions src/components/data-board/BoardApi.composable.unit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { ContentElementType } from "@/serverApi/v3";
import {
ContentElementType,
ExternalToolElementResponse,
} from "@/serverApi/v3";
import * as serverApi from "@/serverApi/v3/api";
import { CardResponse } from "@/serverApi/v3/api";
import { ApplicationError } from "@/store/types/application-error";
Expand Down Expand Up @@ -187,7 +190,7 @@ describe("BoardApi.composable", () => {

it("should call elementControllerUpdateElement api with ExternalToolElement", async () => {
const { updateElementCall } = useBoardApi();
const payload = {
const payload: ExternalToolElementResponse = {
id: "external-tool-element-id",
type: ContentElementType.ExternalTool,
content: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
ToolApiFactory,
ToolApiInterface,
ToolReferenceResponse,
} from "@/serverApi/v3";
import { ExternalToolDisplayData } from "@/store/external-tool";
import { ExternalToolMapper } from "@/store/external-tool/mapper";
import { $axios } from "@/utils/api";
import { AxiosResponse } from "axios";

export const useContextExternalToolApi = () => {
const toolApi: ToolApiInterface = ToolApiFactory(undefined, "/v3", $axios);

const fetchDisplayDataCall = async (
contextExternalToolId: string
): Promise<ExternalToolDisplayData> => {
const response: AxiosResponse<ToolReferenceResponse> =
await toolApi.toolReferenceControllerGetToolReference(
contextExternalToolId
);

const mapped: ExternalToolDisplayData =
ExternalToolMapper.mapToExternalToolDisplayData(response.data);

return mapped;
};

return {
fetchDisplayDataCall,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as serverApi from "@/serverApi/v3/api";
import { ToolReferenceResponse } from "@/serverApi/v3/api";
import {
ExternalToolDisplayData,
ToolConfigurationStatus,
} from "@/store/external-tool";
import {
mockApiResponse,
toolReferenceResponseFactory,
} from "@@/tests/test-utils";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { useContextExternalToolApi } from "./ContextExternalToolApi.composable";

describe("ContextExternalToolApi.composable", () => {
let toolApi: DeepMocked<serverApi.ToolApiInterface>;

beforeEach(() => {
toolApi = createMock<serverApi.ToolApiInterface>();

jest.spyOn(serverApi, "ToolApiFactory").mockReturnValue(toolApi);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("fetchDisplayDataCall", () => {
const setup = () => {
const displayData: ToolReferenceResponse =
toolReferenceResponseFactory.build({ logoUrl: "mockLogoUrl" });

toolApi.toolReferenceControllerGetToolReference.mockResolvedValue(
mockApiResponse({ data: displayData })
);

return {
displayData,
};
};

it("should call the api for tool references", async () => {
setup();

await useContextExternalToolApi().fetchDisplayDataCall(
"contextExternalToolId"
);

expect(
toolApi.toolReferenceControllerGetToolReference
).toHaveBeenCalledWith("contextExternalToolId");
});

it("should return an array of display data", async () => {
const { displayData } = setup();

const result: ExternalToolDisplayData =
await useContextExternalToolApi().fetchDisplayDataCall(
"contextExternalToolId"
);

expect(result).toEqual<ExternalToolDisplayData>({
contextExternalToolId: displayData.contextToolId,
name: displayData.displayName,
logoUrl: displayData.logoUrl,
status: ToolConfigurationStatus.Latest,
openInNewTab: displayData.openInNewTab,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ref, Ref } from "vue";
import { ExternalToolDisplayData } from "../../store/external-tool";
import { useErrorHandler } from "../error-handling/ErrorHandler.composable";
import { useContextExternalToolApi } from "./index";

export const useExternalToolElementDisplayState = () => {
const { handleError } = useErrorHandler();
const { fetchDisplayDataCall } = useContextExternalToolApi();
const displayData: Ref<ExternalToolDisplayData | undefined> = ref();
const isLoading: Ref<boolean> = ref(false);

const fetchDisplayData = async (
contextExternalToolId: string
): Promise<void> => {
isLoading.value = true;

try {
displayData.value = await fetchDisplayDataCall(contextExternalToolId);
} catch (error) {
handleError(error);
}

isLoading.value = false;
};

return {
isLoading,
displayData,
fetchDisplayData,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ExternalToolDisplayData } from "@/store/external-tool";
import { externalToolDisplayDataFactory } from "@@/tests/test-utils";
import { createMock, DeepMocked } from "@golevelup/ts-jest";
import { useErrorHandler } from "../error-handling/ErrorHandler.composable";
import { useExternalToolElementDisplayState } from "./ExternalToolElementDisplayState.composable";
import { useContextExternalToolApi } from "./index";

jest.mock("@data-external-tool");
jest.mock("@/components/error-handling/ErrorHandler.composable");

describe("ExternalToolElementDisplayState.composable", () => {
let useContextExternalToolApiMock: DeepMocked<
ReturnType<typeof useContextExternalToolApi>
>;
let useErrorHandlerMock: DeepMocked<ReturnType<typeof useErrorHandler>>;

beforeEach(() => {
useContextExternalToolApiMock =
createMock<ReturnType<typeof useContextExternalToolApi>>();
useErrorHandlerMock = createMock<ReturnType<typeof useErrorHandler>>();

jest
.mocked(useContextExternalToolApi)
.mockReturnValue(useContextExternalToolApiMock);
jest.mocked(useErrorHandler).mockReturnValue(useErrorHandlerMock);
});

afterEach(() => {
jest.clearAllMocks();
});

describe("when no data is loaded", () => {
it("should not have data", async () => {
const { displayData } = useExternalToolElementDisplayState();

expect(displayData.value).toBeUndefined();
});
});

describe("when data is loaded", () => {
const setup = () => {
const displayDataMock: ExternalToolDisplayData =
externalToolDisplayDataFactory.build();

useContextExternalToolApiMock.fetchDisplayDataCall.mockResolvedValue(
displayDataMock
);

return {
displayDataMock,
...useExternalToolElementDisplayState(),
};
};

it("should call the api for display data of the card", async () => {
const { fetchDisplayData } = setup();

await fetchDisplayData("contextId");

expect(
useContextExternalToolApiMock.fetchDisplayDataCall
).toHaveBeenCalledWith("contextId");
});

it("should set the display data in the state", async () => {
const { fetchDisplayData, displayData, displayDataMock } = setup();

await fetchDisplayData("contextId");

expect(displayData.value).toEqual(displayDataMock);
});
});

describe("when an error occurs during loading", () => {
const setup = () => {
const error = new Error("unable to load");

useContextExternalToolApiMock.fetchDisplayDataCall.mockRejectedValue(
error
);

return {
error,
...useExternalToolElementDisplayState(),
};
};

it("should handle the error", async () => {
const { fetchDisplayData, error } = setup();

await fetchDisplayData("contextId");

expect(useErrorHandlerMock.handleError).toHaveBeenCalledWith(error);
});
});
});
2 changes: 2 additions & 0 deletions src/components/data-external-tool/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ContextExternalToolApi.composable";
export * from "./ExternalToolElementDisplayState.composable";
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</v-autocomplete>
<h2
v-if="
displaySettingsTitle &&
selectedTemplate &&
(!isAboveParametersSlotEmpty || selectedTemplate.parameters.length > 0)
"
Expand Down Expand Up @@ -75,10 +76,12 @@
<script lang="ts">
import ExternalToolConfigSettings from "@/components/external-tools/configuration/ExternalToolConfigSettings.vue";
import { useExternalToolMappings } from "@/composables/external-tool-mappings.composable";
import { useI18n } from "@/composables/i18n.composable";
import {
ExternalToolConfigurationTemplate,
SchoolExternalTool,
} from "@/store/external-tool";
import { ContextExternalTool } from "@/store/external-tool/context-external-tool";
import { BusinessError } from "@/store/types/commons";
import {
computed,
Expand All @@ -91,9 +94,7 @@ import {
useSlots,
watch,
} from "vue";
import { useI18n } from "@/composables/i18n.composable";
import ExternalToolSelectionRow from "./ExternalToolSelectionRow.vue";
import { ContextExternalTool } from "@/store/external-tool/context-external-tool";
type ConfigurationTypes = SchoolExternalTool | ContextExternalTool;
Expand All @@ -117,6 +118,10 @@ export default defineComponent({
loading: {
type: Boolean,
},
displaySettingsTitle: {
type: Boolean,
default: true,
},
},
setup(props, { emit }) {
const { t } = useI18n();
Expand Down
Loading

0 comments on commit 0341a18

Please sign in to comment.