diff --git a/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/AbstractPluginService.java b/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/AbstractPluginService.java index cd21f92ee1..5baa49663a 100644 --- a/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/AbstractPluginService.java +++ b/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/AbstractPluginService.java @@ -190,6 +190,10 @@ public void ensureActivated(PluginDescriptor plugin) { } } + public boolean isActivated(String pluginId) { + return pluginManager.getRegistry().isPluginDescriptorAvailable(pluginId); + } + @SuppressWarnings("nls") @Override public ExtensionPoint getExtensionPoint(String pluginId, String pointId) { diff --git a/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/PluginService.java b/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/PluginService.java index 4d174cb7f0..7cdb5c37d7 100644 --- a/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/PluginService.java +++ b/Platform/Plugins/com.tle.platform.common/src/com/tle/core/plugins/PluginService.java @@ -52,6 +52,16 @@ public interface PluginService { void ensureActivated(PluginDescriptor plugin); + /** + * Check if a plugin is activated. + * + * @param pluginId The ID of plugin + * @return `true` if plugin available otherwise false + */ + default boolean isActivated(String pluginId) { + throw new UnsupportedOperationException(); + } + void registerExtensionListener( String pluginId, String extensionId, RegistryChangeListener listener); diff --git a/Source/Plugins/Core/com.equella.core/js/__mocks__/GallerySearchModule.mock.ts b/Source/Plugins/Core/com.equella.core/js/__mocks__/GallerySearchModule.mock.ts index 0db9aa8a7c..31ce13850f 100644 --- a/Source/Plugins/Core/com.equella.core/js/__mocks__/GallerySearchModule.mock.ts +++ b/Source/Plugins/Core/com.equella.core/js/__mocks__/GallerySearchModule.mock.ts @@ -42,6 +42,7 @@ export const basicImageSearchResponse: OEQ.Search.SearchResult; + +export const fileAttachmentStory: Story = (args) => ( + +); + +fileAttachmentStory.args = { + attachment: fileAttachment, +}; + +export const brokenFileAttachmentStory: Story = (args) => ( + +); + +brokenFileAttachmentStory.args = { + attachment: brokenFileAttachment, +}; + +export const customResourceAttachmentStory: Story = (args) => ( + +); + +customResourceAttachmentStory.args = { + attachment: resourceFileAttachment, +}; + +export const linkAttachmentStory: Story = (args) => ( + +); + +linkAttachmentStory.args = { + attachment: linkAttachment, +}; + +export const resourceLinkAttachmentStory: Story = (args) => ( + +); + +resourceLinkAttachmentStory.args = { + attachment: resourceLinkAttachment, +}; + +export const equellaItemAttachmentStory: Story = (args) => ( + +); + +equellaItemAttachmentStory.args = { + attachment: equellaItemAttachment, +}; + +export const htmlAttachmentStory: Story = (args) => ( + +); + +htmlAttachmentStory.args = { + attachment: htmlAttachment, +}; + +export const resourceHtmlAttachmentStory: Story = (args) => ( + +); + +resourceHtmlAttachmentStory.args = { + attachment: resourceHtmlAttachment, +}; + +export const placeHolderAttachmentStory: Story = (args) => ( + +); + +placeHolderAttachmentStory.args = { + attachment: fileAttachment, + showPlaceholder: true, +}; diff --git a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/components/OEQThumb.test.tsx b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/components/OEQThumb.test.tsx new file mode 100644 index 0000000000..4f0314431f --- /dev/null +++ b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/components/OEQThumb.test.tsx @@ -0,0 +1,105 @@ +/* + * Licensed to The Apereo Foundation under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * The Apereo Foundation licenses this file to you under the Apache License, + * Version 2.0, (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { queryByLabelText, render } from "@testing-library/react"; +import * as React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { + brokenFileAttachment, + equellaItemAttachment, + fileAttachment, + htmlAttachment, + linkAttachment, + resourceHtmlAttachment, + resourceLinkAttachment, +} from "../../../__mocks__/OEQThumb.mock"; +import OEQThumb from "../../../tsrc/components/OEQThumb"; +import * as OEQ from "@openequella/rest-api-client"; +import { languageStrings } from "../../../tsrc/util/langstrings"; + +describe("", () => { + const thumbLabels = languageStrings.searchpage.thumbnails; + const buildOEQThumb = ( + attachment: OEQ.Search.Attachment, + showPlaceHolder: boolean + ) => + render( + + ); + + it.each<[string, OEQ.Search.Attachment, boolean, string]>([ + [ + "shows the placeholder icon when showPlaceholder is true", + fileAttachment, + true, + thumbLabels.placeholder, + ], + [ + "shows thumbnail image when showPlaceholder is false", + fileAttachment, + false, + thumbLabels.provided, + ], + [ + "shows default file thumbnail when brokenAttachment is true", + brokenFileAttachment, + false, + thumbLabels.file, + ], + [ + "shows link icon thumbnail for a link attachment", + linkAttachment, + false, + thumbLabels.link, + ], + [ + "shows link icon thumbnail for a resource link attachment", + resourceLinkAttachment, + false, + thumbLabels.link, + ], + [ + "shows equella item thumbnail for a resource attachment pointing at an item summary", + equellaItemAttachment, + false, + thumbLabels.item, + ], + [ + "shows html thumbnail for a web page attachment", + htmlAttachment, + false, + thumbLabels.html, + ], + [ + "shows html thumbnail for a resource web page attachment", + resourceHtmlAttachment, + false, + thumbLabels.html, + ], + ])( + "%s", + ( + _: string, + attachment: OEQ.Search.Attachment, + showPlaceHolder: boolean, + query: string + ) => { + const { container } = buildOEQThumb(attachment, showPlaceHolder); + expect(queryByLabelText(container, query)).toBeInTheDocument(); + } + ); +}); diff --git a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/GallerySearchModule.test.ts b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/GallerySearchModule.test.ts index 5c1c017485..b40c2fb93a 100644 --- a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/GallerySearchModule.test.ts +++ b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/GallerySearchModule.test.ts @@ -62,6 +62,7 @@ describe("buildGalleryEntry", () => { preview: false, mimeType: "image/png", hasGeneratedThumb: true, + brokenAttachment: false, links: { view: "https://example.com/inst/items/1eeb3df5-3809-4655-925b-24d994e42ff6/1/?attachment.uuid=7186d40d-6159-4d07-8eee-4f7ee0cfdc4e", @@ -75,6 +76,7 @@ describe("buildGalleryEntry", () => { id: "b18ed9ab-1ddb-4961-8935-22bbf1095b24", description: "The Odyssey by Homer | Summary & Analysis", preview: false, + brokenAttachment: false, links: { view: "https://example.com/inst/items/234b9bd6-b603-4e26-8214-b79b8aab0ed9/1/?attachment.uuid=b18ed9ab-1ddb-4961-8935-22bbf1095b24", @@ -151,6 +153,7 @@ describe("buildGallerySearchResultItem", () => { preview: false, mimeType: "image/jpeg", hasGeneratedThumb: true, + brokenAttachment: false, links: { view: "https://example.com/inst/items/535e4e9b-4836-4011-8857-eb29260bf155/1/?attachment.uuid=e7e84411-7cc6-4516-9bc8-d60dab47fccb", @@ -166,6 +169,7 @@ describe("buildGallerySearchResultItem", () => { preview: false, mimeType: "image/jpeg", hasGeneratedThumb: true, + brokenAttachment: false, links: { view: "https://example.com/inst/items/535e4e9b-4836-4011-8857-eb29260bf155/1/?attachment.uuid=17f3036e-a3c6-4e6d-85cb-aaa78ca2835b", @@ -181,6 +185,7 @@ describe("buildGallerySearchResultItem", () => { preview: false, mimeType: "video/mp4", hasGeneratedThumb: true, + brokenAttachment: false, links: { view: "https://example.com/inst/items/535e4e9b-4836-4011-8857-eb29260bf155/1/?attachment.uuid=16a3d193-430c-45f2-bd0c-6a3fc0a3bcaf", diff --git a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/ViewerModule.test.ts b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/ViewerModule.test.ts index dffa6c5200..0e626fa594 100644 --- a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/ViewerModule.test.ts +++ b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/modules/ViewerModule.test.ts @@ -29,11 +29,14 @@ describe("determineViewer()", () => { it("returns link viewer details for non-file attachment", () => { const testLink = "http://some.link/blah"; - expect(determineViewer("blah", testLink)).toEqual([linkViewerId, testLink]); + expect(determineViewer("blah", testLink, false)).toEqual([ + linkViewerId, + testLink, + ]); }); it("returns a 'link' viewer if full parameters aren't provided for 'file' attachments", () => { - const [viewer] = determineViewer(fileAttachmentType, fileViewUrl); + const [viewer] = determineViewer(fileAttachmentType, fileViewUrl, false); expect(viewer).toEqual(linkViewerId); }); @@ -41,6 +44,7 @@ describe("determineViewer()", () => { const [viewer, url] = determineViewer( fileAttachmentType, fileViewUrl, + false, "not/used", "save" ); @@ -55,6 +59,7 @@ describe("determineViewer()", () => { determineViewer( fileAttachmentType, fileViewUrl, + false, "audio/x-mp3", // rather than test every supported MIME type, just using one to keep things short mimeTypeViewerId ) diff --git a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/search/components/SearchResult.test.tsx b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/search/components/SearchResult.test.tsx index 3fe0b77bbc..545e199d7f 100644 --- a/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/search/components/SearchResult.test.tsx +++ b/Source/Plugins/Core/com.equella.core/js/__tests__/tsrc/search/components/SearchResult.test.tsx @@ -257,6 +257,44 @@ describe("", () => { ); }); + describe("Dead attachments handling", () => { + it("should display dead attachments with a warning label", async () => { + const { oneDeadAttachObj } = mockData; + const { queryByTitle } = await renderSearchResult(oneDeadAttachObj); + expect( + queryByTitle(languageStrings.searchpage.deadAttachmentWarning) + ).toBeInTheDocument(); + }); + + it("should not render dead attachments as clickable links", async () => { + //item with one dead attachment and one intact attachment + const { oneDeadOneAliveAttachObj } = mockData; + const { getByText, queryByLabelText } = await renderSearchResult( + oneDeadOneAliveAttachObj + ); + + // Given a user clicks on a broken attachment + userEvent.click( + getByText(oneDeadOneAliveAttachObj.attachments![0].description!) + ); + + // There is no lightbox, as it is not rendered as a link + expect( + queryByLabelText(languageStrings.common.action.openInNewWindow) + ).not.toBeInTheDocument(); + + // Now if they click on the intact attachment instead... + userEvent.click( + getByText(oneDeadOneAliveAttachObj.attachments![1].description!) + ); + + // ...There is a lightbox + expect( + queryByLabelText(languageStrings.common.action.openInNewWindow) + ).toBeInTheDocument(); + }); + }); + describe("In Selection Session", () => { beforeAll(() => { updateMockGlobalCourseList(); @@ -397,6 +435,25 @@ describe("", () => { }); }); + it("should not make broken attachments draggable", async () => { + updateMockGetRenderData(basicRenderData); + await renderSearchResult(mockData.oneDeadOneAliveAttachObj); + + const deadAttachment = mockData.oneDeadOneAliveAttachObj.attachments![0]; + const intactAttachment = mockData.oneDeadOneAliveAttachObj + .attachments![1]; + + //expect intact attachment to be draggable + expect( + getGlobalCourseList().prepareDraggableAndBind + ).toHaveBeenCalledWith(`#${intactAttachment.id}`, false); + + //expect dead attachment not to be draggable + expect( + getGlobalCourseList().prepareDraggableAndBind + ).not.toHaveBeenCalledWith(`#${deadAttachment.id}`, false); + }); + it("should hide All attachment button in Skinny", async () => { updateMockGetRenderData(renderDataForSkinny); const { queryByLabelText } = await renderSearchResult( @@ -406,5 +463,49 @@ describe("", () => { queryByLabelText(selectAllAttachmentsString) ).not.toBeInTheDocument(); }); + + describe("Dead attachments handling", () => { + it("Should not be possible to select a dead attachment", async () => { + updateMockGetRenderData({ + ...basicRenderData, + selectionSessionInfo: selectSummaryButtonDisabled, + }); + const { queryByLabelText } = await renderSearchResult( + mockData.oneDeadAttachObj + ); + expect( + queryByLabelText(selectAttachmentString) + ).not.toBeInTheDocument(); + }); + + it("Should not show the Select All Attachments button if all attachments are dead", async () => { + const { queryByLabelText } = await renderSearchResult( + mockData.oneDeadAttachObj + ); + expect( + queryByLabelText(selectAllAttachmentsString) + ).not.toBeInTheDocument(); + }); + + it("Should show the Select All Attachments button if at least one attachment is not dead", async () => { + const { queryByLabelText, getByTitle } = await renderSearchResult( + mockData.oneDeadOneAliveAttachObj + ); + expect( + queryByLabelText(selectAllAttachmentsString) + ).toBeInTheDocument(); + // Given the user clicks Select All Attachments for an item with a dead attachment + // and an alive attachment... + userEvent.click(getByTitle(selectAllAttachmentsString)); + + // The function should only have been called with the attachment + // 78883eff-7cf6-4b14-ab76-2b7f84dbe833 which is the intact one + expect( + mockSelectResourceForCourseList + ).toHaveBeenCalledWith("72558c1d-8788-4515-86c8-b24a28cc451e/1", [ + "78883eff-7cf6-4b14-ab76-2b7f84dbe833", + ]); + }); + }); }); }); diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/components/ItemAttachmentLink.tsx b/Source/Plugins/Core/com.equella.core/js/tsrc/components/ItemAttachmentLink.tsx index 4dd8d28394..144fe2b927 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/components/ItemAttachmentLink.tsx +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/components/ItemAttachmentLink.tsx @@ -15,13 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Link } from "@material-ui/core"; +import { Link, Typography } from "@material-ui/core"; import * as React from "react"; import { SyntheticEvent, useState } from "react"; import { AttachmentAndViewerConfig, isViewerLightboxConfig, ViewerLightboxConfig, + ViewerLinkConfig, } from "../modules/ViewerModule"; import { languageStrings } from "../util/langstrings"; import Lightbox, { LightboxProps } from "./Lightbox"; @@ -47,13 +48,29 @@ export interface ItemAttachmentLinkProps { const ItemAttachmentLink = ({ children, selectedAttachment: { - attachment: { description, mimeType }, + attachment: { description, mimeType, brokenAttachment }, viewerConfig, }, }: ItemAttachmentLinkProps) => { const { attachmentLink } = languageStrings.searchpage.searchResult; const [lightBoxProps, setLightBoxProps] = useState(); + const buildSimpleLink = (viewerConfig: ViewerLinkConfig): JSX.Element => { + return brokenAttachment ? ( + + {description} + + ) : ( + + {children} + + ); + }; const buildLightboxLink = ({ config }: ViewerLightboxConfig): JSX.Element => { if (!mimeType) { throw new Error( @@ -85,19 +102,11 @@ const ItemAttachmentLink = ({ ); }; - return isViewerLightboxConfig(viewerConfig) ? ( - buildLightboxLink(viewerConfig) - ) : ( - // Lightbox viewer not specified, so go with the default of a simple link. - - {children} - - ); + + return isViewerLightboxConfig(viewerConfig) + ? buildLightboxLink(viewerConfig) + : // Lightbox viewer not specified, so go with the default of a simple link. + buildSimpleLink(viewerConfig); }; export default ItemAttachmentLink; diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/components/OEQThumb.tsx b/Source/Plugins/Core/com.equella.core/js/tsrc/components/OEQThumb.tsx index 47ffce5450..e0d6e53bd3 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/components/OEQThumb.tsx +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/components/OEQThumb.tsx @@ -26,6 +26,7 @@ import DefaultFileIcon from "@material-ui/icons/InsertDriveFile"; import WebIcon from "@material-ui/icons/Language"; import Web from "@material-ui/icons/Web"; import PlaceholderIcon from "@material-ui/icons/TextFields"; +import { languageStrings } from "../util/langstrings"; const useStyles = makeStyles((theme: Theme) => { return { @@ -48,7 +49,7 @@ interface ThumbProps { fontSize: "inherit" | "default" | "small" | "large"; } -interface OEQThumbProps { +export interface OEQThumbProps { /** * On object representing an oEQ attachment. If undefined, a placeholder icon is returned */ @@ -70,13 +71,19 @@ export default function OEQThumb({ showPlaceholder, }: OEQThumbProps) { const classes = useStyles(); + const thumbLabels = languageStrings.searchpage.thumbnails; const generalThumbStyles: ThumbProps = { className: `MuiPaper-elevation1 MuiPaper-rounded ${classes.thumbnail} ${classes.placeholderThumbnail}`, fontSize: "large", }; if (!attachment || showPlaceholder) { - return ; + return ( + + ); } const { @@ -89,13 +96,16 @@ export default function OEQThumb({ const oeqProvidedThumb: React.ReactElement = ( {description} ); - const defaultThumb = ; + const defaultThumb = ( + + ); /** * We need to check if a thumbnail has been generated, and return a generic icon if not @@ -108,32 +118,60 @@ export default function OEQThumb({ } let result = defaultThumb; if (mimeType?.startsWith("image")) { - result = ; + result = ( + + ); } else if (mimeType?.startsWith("video")) { - result = ; + result = ( + + ); } return result; }; - let oeqThumb = defaultThumb; + /** + * Resource attachments point to other attachments or items, so we need to use the MIME type + * to determine the thumbnail to use, rather than the attachment type which will be custom/resource. + * + * @param mimeType The MIME type of the resource attachment's target. + */ + const handleResourceAttachmentThumb = (mimeType?: string) => { + switch (mimeType) { + case "equella/item": + return ; + case "equella/link": + return ( + + ); + case "text/html": + return ( + + ); + default: + return oeqProvidedThumb; + } + }; + let oeqThumb = defaultThumb; + if (attachment.brokenAttachment) { + return defaultThumb; + } switch (attachmentType) { case "file": oeqThumb = handleMimeType(mimeType); break; case "link": - oeqThumb = ; + oeqThumb = ( + + ); break; case "html": - oeqThumb = ; + oeqThumb = ( + + ); break; case "custom/resource": - oeqThumb = - mimeType === "equella/item" ? ( - - ) : ( - oeqProvidedThumb - ); + oeqThumb = handleResourceAttachmentThumb(mimeType); break; case "custom/flickr": case "custom/youtube": diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/modules/GallerySearchModule.ts b/Source/Plugins/Core/com.equella.core/js/tsrc/modules/GallerySearchModule.ts index a53e335133..c997aef010 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/modules/GallerySearchModule.ts +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/modules/GallerySearchModule.ts @@ -508,7 +508,7 @@ export const videoGallerySearch = async ( options: SearchOptions ): Promise> => gallerySearch( - { ...options, musts: videoGalleryMusts }, + { ...options, musts: videoGalleryMusts, mimeTypes: undefined }, filterAttachmentsByVideo ); @@ -534,4 +534,8 @@ export const listImageGalleryClassifications = async ( export const listVideoGalleryClassifications = async ( options: SearchOptions ): Promise => - listClassifications({ ...options, musts: videoGalleryMusts }); + listClassifications({ + ...options, + musts: videoGalleryMusts, + mimeTypes: undefined, + }); diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/modules/ViewerModule.ts b/Source/Plugins/Core/com.equella.core/js/tsrc/modules/ViewerModule.ts index 646173f949..12b924c2c3 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/modules/ViewerModule.ts +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/modules/ViewerModule.ts @@ -87,15 +87,19 @@ export interface AttachmentAndViewerConfig { * @param viewUrl the basic view URL returned in search results * @param mimeType the MIME type of the attachment to determine the viewer for * @param mimeTypeViewerId the server specified `ViewerId` for the attachment + * @param broken whether or not this has been marked as a broken attachment by the server */ export const determineViewer = ( attachmentType: string, viewUrl: string, + broken: boolean, mimeType?: string, mimeTypeViewerId?: OEQ.MimeType.ViewerId ): ViewerDefinition => { const simpleLinkView: ViewerDefinition = ["link", viewUrl]; - + if (broken) { + return simpleLinkView; + } if (attachmentType !== "file" && attachmentType !== "custom/resource") { // For non-file attachments, we currently just defer to the link provided by the server return simpleLinkView; @@ -163,6 +167,7 @@ export const getViewerDefinitionForAttachment = ( mimeType, links: { view: defaultViewUrl }, filePath, + brokenAttachment, } = attachment; const viewUrl = determineAttachmentViewUrl( itemUuid, @@ -172,7 +177,13 @@ export const getViewerDefinitionForAttachment = ( filePath ); - return determineViewer(attachmentType, viewUrl, mimeType, mimeTypeViewerId); + return determineViewer( + attachmentType, + viewUrl, + brokenAttachment, + mimeType, + mimeTypeViewerId + ); }; /** diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/search/components/SearchResultAttachmentsList.tsx b/Source/Plugins/Core/com.equella.core/js/tsrc/search/components/SearchResultAttachmentsList.tsx index d78f90c55a..4b04b68ed2 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/search/components/SearchResultAttachmentsList.tsx +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/search/components/SearchResultAttachmentsList.tsx @@ -37,6 +37,7 @@ import DragIndicatorIcon from "@material-ui/icons/DragIndicator"; import ExpandMore from "@material-ui/icons/ExpandMore"; import InsertDriveFile from "@material-ui/icons/InsertDriveFile"; import Search from "@material-ui/icons/Search"; +import Warning from "@material-ui/icons/Warning"; import * as OEQ from "@openequella/rest-api-client"; import * as React from "react"; import { SyntheticEvent, useEffect, useState } from "react"; @@ -121,12 +122,14 @@ export const SearchResultAttachmentsList = ({ setAttachmentsAndViewerConfigs, ] = useState([]); - // In Selection Session, make each attachment draggable. + // In Selection Session, make each intact attachment draggable. useEffect(() => { if (inStructured) { - attachmentsAndViewerConfigs.forEach(({ attachment }) => { - prepareDraggable(attachment.id, false); - }); + attachmentsAndViewerConfigs + .filter(({ attachment }) => !attachment.brokenAttachment) + .forEach(({ attachment }) => { + prepareDraggable(attachment.id, false); + }); } }, [attachmentsAndViewerConfigs, inStructured]); @@ -138,8 +141,11 @@ export const SearchResultAttachmentsList = ({ return; } - const getViewerID = async (mimeType: string) => { + const getViewerID = async (broken: boolean, mimeType: string) => { let viewerDetails: OEQ.MimeType.MimeTypeViewerDetail | undefined; + if (broken) { + return undefined; + } try { viewerDetails = await getViewerDetails(mimeType); } catch (error) { @@ -155,8 +161,10 @@ export const SearchResultAttachmentsList = ({ const attachmentsAndViewerDefinitions = await Promise.all( attachments.map>( async (attachment) => { - const { mimeType } = attachment; - const viewerId = mimeType ? await getViewerID(mimeType) : undefined; + const { mimeType, brokenAttachment } = attachment; + const viewerId = mimeType + ? await getViewerID(brokenAttachment, mimeType) + : undefined; return { attachment, viewerDefinition: getViewerDefinitionForAttachment( @@ -234,14 +242,33 @@ export const SearchResultAttachmentsList = ({ setAttachExpanded(!attachExpanded); }; + const buildIcon = (broken: boolean) => { + if (broken) { + return ( + + + + ); + } + return inStructured ? : ; + }; + + const isAttachmentSelectable = (broken: boolean) => + inSelectionSession && !broken; + const attachmentsList = attachmentsAndViewerConfigs.map( (attachmentAndViewerConfig: AttachmentAndViewerConfig) => { const { - attachment: { id, description }, + attachment: { id, description, brokenAttachment }, } = attachmentAndViewerConfig; return ( { + if (brokenAttachment) { + event.stopPropagation(); + } + }} key={id} id={id} button @@ -250,13 +277,11 @@ export const SearchResultAttachmentsList = ({ data-itemversion={version} data-attachmentuuid={id} > - - {inStructured ? : } - + {buildIcon(brokenAttachment)} - {inSelectionSession && ( + {isAttachmentSelectable(brokenAttachment) && ( ); + // Only show the Select All Attachments button if at least one attachment is not dead + const atLeastOneIntactAttachment = attachmentsAndViewerConfigs.some( + ({ attachment }) => !attachment.brokenAttachment + ); + + const showSelectAllAttachments = atLeastOneIntactAttachment && !inSkinny; + const accordionSummaryContent = inSelectionSession ? ( {accordionText} - {!inSkinny && ( + {showSelectAllAttachments && ( { - const attachments = attachmentsAndViewerConfigs.map( - ({ attachment }) => attachment.id - ); + const attachments = attachmentsAndViewerConfigs + .filter( + // filter out dead attachments from select all function + ({ attachment }) => !attachment.brokenAttachment + ) + .map(({ attachment }) => attachment.id); handleSelectResource(itemKey, attachments); }} /> diff --git a/Source/Plugins/Core/com.equella.core/js/tsrc/util/langstrings.ts b/Source/Plugins/Core/com.equella.core/js/tsrc/util/langstrings.ts index 7cdb592ba5..6f2752f376 100644 --- a/Source/Plugins/Core/com.equella.core/js/tsrc/util/langstrings.ts +++ b/Source/Plugins/Core/com.equella.core/js/tsrc/util/langstrings.ts @@ -330,6 +330,8 @@ export const languageStrings = { newSearchHelperText: "Clears search text and filters", shareSearchHelperText: "Copy search link to clipboard", shareSearchConfirmationText: "Search link saved to clipboard", + deadAttachmentWarning: + "This attachment appears to be broken or inaccessible.", displayModeSelector: { title: "Display mode", modeItemList: "Item List", @@ -449,6 +451,16 @@ export const languageStrings = { live: "Live", title: "Status", }, + thumbnails: { + html: "HTML Icon", + placeholder: "Placeholder Icon", + provided: "Provided Icon", + file: "Default File Icon", + image: "Image Icon", + video: "Video Icon", + link: "Link Icon", + item: "Item Icon", + }, comments: { zero: "No comments", one: "%d comment", diff --git a/Source/Plugins/Core/com.equella.core/plugin-jpf.xml b/Source/Plugins/Core/com.equella.core/plugin-jpf.xml index fb89dfe1c2..f624f92ea4 100644 --- a/Source/Plugins/Core/com.equella.core/plugin-jpf.xml +++ b/Source/Plugins/Core/com.equella.core/plugin-jpf.xml @@ -6061,6 +6061,11 @@ + + + + + diff --git a/Source/Plugins/Core/com.equella.core/resources/lang/i18n-resource-centre.properties b/Source/Plugins/Core/com.equella.core/resources/lang/i18n-resource-centre.properties index 425839efa3..e2a2c47090 100644 --- a/Source/Plugins/Core/com.equella.core/resources/lang/i18n-resource-centre.properties +++ b/Source/Plugins/Core/com.equella.core/resources/lang/i18n-resource-centre.properties @@ -81,6 +81,7 @@ /com.tle.core.entity.services.migration.v20202.facetedsearch.classification=Create a new table for faceted search classification /com.tle.core.entity.services.migration.v20202.removelastknownuserconstraint=Remove last known user composite constraint (username and institution ID) /com.tle.core.entity.services.migration.v20202.indexing.errored=Add column to attachment table to allow indexer to skip individual attachments that have failed. +/com.tle.core.entity.services.migration.v20211.enable.default.viewer=Ensure configured default viewers are also enabled /com.tle.core.entity.services.query.contains={0} is {1} /com.tle.core.entity.services.query.date.after={0} after {1} /com.tle.core.entity.services.query.date.before={0} before {1} @@ -2746,6 +2747,7 @@ mimetypes.migration.title=Update default MIME Type icons mimetypes.settings.description=Display a list of MIME types, where the properties can be edited, including viewer defaults mimetypes.settings.title=MIME types mimetypes.title=MIME type editor +mimetypes.default.viewer.not.enabled=Default viewer is not enabled missingprivileges=You do not have the required privilege to access this page\: {0} moddialog.accepted=Moderators already accepted moddialog.and=and diff --git a/Source/Plugins/Core/com.equella.core/resources/web/sass/legacy.scss b/Source/Plugins/Core/com.equella.core/resources/web/sass/legacy.scss index 39c7460c4a..1e9ddf0eaa 100644 --- a/Source/Plugins/Core/com.equella.core/resources/web/sass/legacy.scss +++ b/Source/Plugins/Core/com.equella.core/resources/web/sass/legacy.scss @@ -280,6 +280,30 @@ $savePanelWidth: 221px; padding-left: $width; } +@mixin standardButton { + @include curvedCorners; + padding: 0 $singleMargin 0 $singleMargin; + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + box-sizing: border-box; + min-width: 64px; + border: none; + outline: none; + cursor: pointer; + background-color: $primaryColor; + color: set-text-color($primaryColor); + @include typography-button(); + + &:hover { + background-color: transparentize($primaryColor, 0.1); + } + &:active { + background-color: transparentize($primaryColor, 0.3); + } +} + /***************************************************************************** Animations *****************************************************************************/ @@ -420,27 +444,7 @@ Buttons *****************************************************************************/ .btn { - @include curvedCorners; - padding: 0 $singleMargin 0 $singleMargin; - display: inline-flex; - position: relative; - align-items: center; - justify-content: center; - box-sizing: border-box; - min-width: 64px; - border: none; - outline: none; - cursor: pointer; - background-color: $primaryColor; - color: set-text-color($primaryColor); - @include typography-button(); - - &:hover { - background-color: transparentize($primaryColor, 0.1); - } - &:active { - background-color: transparentize($primaryColor, 0.3); - } + @include standardButton; } .button-expandable { @@ -4262,7 +4266,8 @@ a.modal-control.modal-save { } } -.urlHandler { +.urlHandler, +.kalturaHandler { .modal-content .modal-content-background .modal-content-inner .float-left { @include editAttachmentDialogStyles; } @@ -4272,6 +4277,66 @@ a.modal-control.modal-save { } } +.kalturaHandler { + @include buttonShadowBorder; + + .choice.odd { + background-color: white; + } + + .choice.even { + background-color: #f3f1eb; + } + + img#kaltura-logo { + position: absolute; + bottom: 25px; + right: 35px; + opacity: 0.35; + } + + .choice-list div { + position: relative; + padding: $doubleMargin; + } + + div.choice-list { + border: 1px solid #8f8b7e; + } + + div.choice-list .choice { + cursor: pointer; + } + + div.choice-list .choice:hover { + background-color: #ffffcc; + } + + div.choice-list .choice.selected { + background-color: #80d2ee; + } + + .choice h4 { + font-size: 12px; + padding-bottom: $minimalMargin; + } + + #kaltura-query button { + background-color: $primaryColor; + color: set-text-color($primaryColor); + padding-left: $singleMargin; + padding-right: $singleMargin; + margin-left: $singleMargin; + } + + #kaltura-simple-uploader { + input[type="submit"], + button { + @include standardButton; + } + } +} + .fileHandler, .urlHandler { @include buttonShadowBorder; diff --git a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/SearchHelper.scala b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/SearchHelper.scala index bb1602a7a0..8b397cc4be 100644 --- a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/SearchHelper.scala +++ b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/SearchHelper.scala @@ -20,13 +20,15 @@ package com.tle.web.api.search import com.dytech.edge.exceptions.BadRequestException import com.tle.beans.entity.DynaCollection -import com.tle.beans.item.{Comment, ItemIdKey} +import com.tle.beans.item.attachments.{Attachment, CustomAttachment, FileAttachment} +import com.tle.beans.item.{Comment, ItemId, ItemIdKey} import com.tle.common.Check import com.tle.common.beans.exception.NotFoundException import com.tle.common.collection.AttachmentConfigConstants import com.tle.common.search.DefaultSearch import com.tle.common.search.whereparser.WhereParser import com.tle.core.freetext.queries.FreeTextBooleanQuery + import com.tle.core.item.security.ItemSecurityConstants import com.tle.core.item.serializer.{ItemSerializerItemBean, ItemSerializerService} import com.tle.core.services.item.{FreetextResult, FreetextSearchResults} @@ -276,17 +278,22 @@ object SearchHelper { beans.asScala // Filter out restricted attachments if the user does not have permissions to view them .filter(a => !a.isRestricted || hasRestrictedAttachmentPrivileges) - .map(att => + .map(att => { + val broken = + recurseBrokenAttachmentCheck( + Option(LegacyGuice.itemService.getNullableAttachmentForUuid(itemKey, att.getUuid))) SearchResultAttachment( attachmentType = att.getRawAttachmentType, id = att.getUuid, description = Option(att.getDescription), + brokenAttachment = broken, preview = att.isPreview, - mimeType = getMimetypeForAttachment(att), + mimeType = getMimetypeForAttachment(att, broken), hasGeneratedThumb = thumbExists(itemKey, att), links = buildAttachmentLinks(att), filePath = getFilePathForAttachment(att) - )) + ) + }) .toList) } @@ -310,10 +317,61 @@ object SearchHelper { } } + /** + * Determines if a given customAttachment is invalid. Required as these attachments can be recursive. + * @param customAttachment The attachment to check. + * @return If true, this attachment is broken. + */ + def getBrokenAttachmentStatusForResourceAttachment( + customAttachment: CustomAttachment): Boolean = { + val key = new ItemId(customAttachment.getData("uuid").asInstanceOf[String], + customAttachment.getData("version").asInstanceOf[Int]) + if (customAttachment.getType != "resource") { + return false; + } + customAttachment.getData("type") match { + case "a" => + // Recurse into child attachment + recurseBrokenAttachmentCheck( + Option( + LegacyGuice.itemService.getNullableAttachmentForUuid(key, customAttachment.getUrl))) + case "p" => + // Get the child item. If it doesn't exist, this is a dead attachment + Option(LegacyGuice.itemService.getUnsecureIfExists(key)).isEmpty + case _ => false + } + } + + /** + * Determines if a given attachment is invalid. + * If it is a resource selector attachment, this gets handled by + * [[getBrokenAttachmentStatusForResourceAttachment(customAttachment: CustomAttachment)]] + * which links back in here to recurse through customAttachments to find the root. + * @param attachment The attachment to check for brokenness. + * @return True if broken, false if intact. + */ + def recurseBrokenAttachmentCheck(attachment: Option[Attachment]): Boolean = { + attachment match { + case Some(fileAttachment: FileAttachment) => + //check if file is present in the filestore + val item = + LegacyGuice.viewableItemFactory.createNewViewableItem(fileAttachment.getItem.getItemId) + !LegacyGuice.fileSystemService.fileExists(item.getFileHandle, fileAttachment.getFilename) + case Some(customAttachment: CustomAttachment) => + getBrokenAttachmentStatusForResourceAttachment(customAttachment) + case None => true + case Some(_) => false + } + } + /** * Extract the mimetype for AbstractExtendableBean. */ - def getMimetypeForAttachment[T <: AbstractExtendableBean](bean: T): Option[String] = { + def getMimetypeForAttachment[T <: AbstractExtendableBean](bean: T, + broken: Boolean): Option[String] = { + if (broken) { + return None + } bean match { case file: AbstractFileAttachmentBean => Some(LegacyGuice.mimeTypeService.getMimeTypeForFilename(file.getFilename)) diff --git a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/model/SearchResultItem.scala b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/model/SearchResultItem.scala index 5aa26a0360..b35b5e40f8 100644 --- a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/model/SearchResultItem.scala +++ b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/search/model/SearchResultItem.scala @@ -69,6 +69,9 @@ case class SearchResultItem( * @param attachmentType Attachment's type. * @param id The unique ID of an attachment. * @param description The description of an attachment. + * @param brokenAttachment If true, this attachment is broken or inaccessible. + * For file attachments, this means that it is not accessible from the filestore. + * For resource selector attachments, this means that the linked attachment or item summary does not exist. * @param preview If an attachment can be previewed or not. * @param mimeType Mime Type of file based attachments * @param hasGeneratedThumb Indicates if file based attachments have a generated thumbnail store in filestore @@ -79,6 +82,7 @@ case class SearchResultAttachment( attachmentType: String, id: String, description: Option[String], + brokenAttachment: Boolean, preview: Boolean, mimeType: Option[String], hasGeneratedThumb: Option[Boolean], diff --git a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/settings/MimeTypeResource.scala b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/settings/MimeTypeResource.scala index 96682b1c4a..4fc31ab492 100644 --- a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/settings/MimeTypeResource.scala +++ b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/api/settings/MimeTypeResource.scala @@ -70,7 +70,6 @@ class MimeTypeResource { responseContainer = "List" ) def listMimeTypes: Response = { - LegacyGuice.mimePrivProvider.checkAuthorised() val mimeEntries = LegacyGuice.mimeTypeService.searchByMimeType(Constants.BLANK, 0, -1).getResults.asScala val mimeTypes = mimeEntries.map( diff --git a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/template/RenderNewTemplate.scala b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/template/RenderNewTemplate.scala index 713014ecd7..310268d9ee 100644 --- a/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/template/RenderNewTemplate.scala +++ b/Source/Plugins/Core/com.equella.core/scalasrc/com/tle/web/template/RenderNewTemplate.scala @@ -19,15 +19,15 @@ package com.tle.web.template import java.util.concurrent.ConcurrentHashMap - import com.tle.common.i18n.{CurrentLocale, LocaleUtils} import com.tle.common.settings.standard.QuickContributeAndVersionSettings import com.tle.core.db.RunWithDB import com.tle.core.i18n.LocaleLookup +import com.tle.core.plugins.AbstractPluginService import com.tle.legacy.LegacyGuice import com.tle.web.DebugSettings import com.tle.web.freemarker.FreemarkerFactory -import com.tle.web.resources.ResourcesService +import com.tle.web.resources.{ResourcesService} import com.tle.web.sections._ import com.tle.web.sections.equella.ScalaSectionRenderable import com.tle.web.sections.events._ @@ -37,6 +37,7 @@ import com.tle.web.sections.js.generic.function.IncludeFile import com.tle.web.sections.render._ import com.tle.web.selection.section.RootSelectionSection import com.tle.web.integration.IntegrationSection +import com.tle.web.sections.render.CssInclude.{Priority, include} import com.tle.web.selection.section.RootSelectionSection.Layout import com.tle.web.settings.UISettings import javax.servlet.http.HttpServletRequest @@ -83,11 +84,24 @@ object RenderNewTemplate { supportIEPolyFills(info) info.preRender(bundleJs) links.foreach(l => info.addCss(r.url(l.attr("href")))) + addKalturaCss(info) info.addCss(RenderTemplate.CUSTOMER_CSS) } (prerender, htmlDoc) } + def addKalturaCss(info: PreRenderContext): Unit = { + val kalturaPluginId = "com.tle.web.wizard.controls.kaltura" + val pluginService = AbstractPluginService.get() + if (pluginService.isActivated(kalturaPluginId)) { + info.addCss( + include( + ResourcesService + .getResourceHelper(kalturaPluginId) + .url("js/UploadControlEntry.css")).priority(Priority.LOWEST).make()) + } + } + val NewLayoutKey = "NEW_LAYOUT" // Check if new UI is enabled. diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/institution/migration/v20211/EnableDefaultViewerMigration.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/institution/migration/v20211/EnableDefaultViewerMigration.java new file mode 100644 index 0000000000..2e221cebbc --- /dev/null +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/institution/migration/v20211/EnableDefaultViewerMigration.java @@ -0,0 +1,111 @@ +/* + * Licensed to The Apereo Foundation under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * The Apereo Foundation licenses this file to you under the Apache License, + * Version 2.0, (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tle.core.institution.migration.v20211; + +import com.google.inject.Singleton; +import com.tle.common.Check; +import com.tle.core.guice.Bind; +import com.tle.core.hibernate.impl.HibernateMigrationHelper; +import com.tle.core.migration.AbstractHibernateDataMigration; +import com.tle.core.migration.MigrationInfo; +import com.tle.core.migration.MigrationResult; +import com.tle.core.mimetypes.MimeTypeConstants; +import com.tle.core.mimetypes.MimeTypeService; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.inject.Inject; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.MapKeyColumn; +import org.hibernate.Session; +import org.hibernate.annotations.AccessType; +import org.hibernate.annotations.MapKeyType; +import org.hibernate.annotations.Type; + +/** + * Some existing MIME types do not have their default viewers enabled. This results in error + * messages showing in the New Search UI. So we use this migration to ensure the default viewer is + * in list of enabled viewers. + */ +@Bind +@Singleton +public class EnableDefaultViewerMigration extends AbstractHibernateDataMigration { + + @Inject private MimeTypeService mimeTypeService; + + @Override + protected void executeDataMigration( + HibernateMigrationHelper helper, MigrationResult result, Session session) throws Exception { + final List entries = session.createQuery("FROM MimeEntry").list(); + for (FakeMimeEntry entry : entries) { + Map attributes = entry.attributes; + // Calling 'getEnabledViewerList' to fix this issue as the list returned from this function + // include the default viewer whereas the one saved in attributes may have default viewer + // missing. + String enabledViewers = mimeTypeService.getEnabledViewerList(attributes); + if (!Check.isEmpty(enabledViewers)) { + attributes.put(MimeTypeConstants.KEY_ENABLED_VIEWERS, enabledViewers); + session.update(entry); + session.flush(); + } + } + result.incrementStatus(); + } + + @Override + protected int countDataMigrations(HibernateMigrationHelper helper, Session session) { + return 0; + } + + @Override + protected Class[] getDomainClasses() { + return new Class[] {FakeMimeEntry.class}; + } + + @Override + public MigrationInfo createMigrationInfo() { + return new MigrationInfo("com.tle.core.entity.services.migration.v20211.enable.default.viewer"); + } + + @Entity(name = "MimeEntry") + @AccessType("field") + public static class FakeMimeEntry { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + long id; + + @ElementCollection + @Column(name = "element", nullable = false) + @CollectionTable( + name = "mime_entry_attributes", + joinColumns = @JoinColumn(name = "mime_entry_id")) + @Lob + @MapKeyColumn(name = "mapkey", length = 100, nullable = false) + @MapKeyType(@Type(type = "string")) + Map attributes = new HashMap(); + } +} diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/AttachmentDao.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/AttachmentDao.java index 8658cea4ce..a1d34701e1 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/AttachmentDao.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/AttachmentDao.java @@ -36,8 +36,4 @@ List findByMd5Sum( List findResourceAttachmentsByQuery( String query, boolean liveOnly, String sortHql); - - Attachment findByUuid(String uuid); - - List findAllByUuid(String uuid); } diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/impl/AttachmentDaoImpl.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/impl/AttachmentDaoImpl.java index 60799f9af8..5a0444f3c2 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/impl/AttachmentDaoImpl.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/dao/impl/AttachmentDaoImpl.java @@ -33,7 +33,6 @@ import java.util.Arrays; import java.util.List; import javax.inject.Singleton; -import org.hibernate.Criteria; import org.hibernate.criterion.DetachedCriteria; import org.hibernate.criterion.Restrictions; @@ -137,14 +136,4 @@ private DetachedCriteria criteriaByUuid(String uuid) { .add(Restrictions.eq("i.institution", CurrentInstitution.get())) .add(Restrictions.eq("uuid", uuid)); } - - @Override - public List findAllByUuid(String uuid) { - return (List) findByDetachedCriteria(criteriaByUuid(uuid), Criteria::list); - } - - @Override - public Attachment findByUuid(String uuid) { - return (Attachment) findByDetachedCriteria(criteriaByUuid(uuid), Criteria::uniqueResult); - } } diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/ItemService.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/ItemService.java index 8f47bcfbc0..61ab68fccd 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/ItemService.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/ItemService.java @@ -66,13 +66,22 @@ public interface ItemService List getNextLiveItems(List items); /** - * @param itemId - * @param uuid - * @return Never returns null. + * @param itemId the Item for which to return an attachment + * @param uuid The UUID of the attachment to return + * @return Never returns null. Returns attachment or throws. * @throws AttachmentNotFoundException */ Attachment getAttachmentForUuid(ItemKey itemId, String uuid); + /** + * As {@link #getAttachmentForUuid(ItemKey itemId, String uuid)}, without the null check. + * + * @param itemId the Item for which to return an attachment + * @param uuid The UUID of the attachment to return + * @return Returns null if attachment not found. + */ + Attachment getNullableAttachmentForUuid(ItemKey itemId, String uuid); + Multimap getAttachmentsForItems(Collection items); int getLatestVersion(String uuid); diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/impl/ItemServiceImpl.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/impl/ItemServiceImpl.java index 0012b5c4fe..706401770b 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/impl/ItemServiceImpl.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/item/service/impl/ItemServiceImpl.java @@ -480,6 +480,11 @@ public Attachment getAttachmentForUuid(ItemKey itemId, String uuid) { return attachment; } + @Override + public Attachment getNullableAttachmentForUuid(ItemKey itemId, String uuid) { + return dao.getAttachmentByUuid(itemId, uuid); + } + @Override public Map getItemInfo(ItemId id) { return dao.getItemInfo(id.getUuid(), id.getVersion()); diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeService.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeService.java index 80f43578c0..589eab1a56 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeService.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeService.java @@ -20,12 +20,14 @@ import com.tle.annotation.NonNullByDefault; import com.tle.annotation.Nullable; +import com.tle.beans.item.ItemId; import com.tle.beans.item.attachments.Attachment; import com.tle.beans.mime.MimeEntry; import com.tle.core.TextExtracterExtension; import com.tle.web.controls.resource.ResourceAttachmentBean; import java.util.Collection; import java.util.List; +import java.util.Map; @NonNullByDefault public interface MimeTypeService { @@ -38,7 +40,7 @@ public interface MimeTypeService { */ String getMimeTypeForFilename(String filename); - String getMimeTypeForAttachmentUuid(String attachmentUuid); + String getMimeTypeForAttachmentUuid(ItemId key, String attachmentUuid); String getMimeTypeForResourceAttachmentBean(ResourceAttachmentBean resourceAttachmentBean); @@ -87,6 +89,17 @@ public interface MimeTypeService { @Nullable String getMimeEntryForAttachment(Attachment attachment); + /** + * Return a list of enabled viewers. If the default viewer does not exist in the list, add it to + * the list. However, if the default viewer is "file", it will not be included because "file" is + * added to the list dynamically by {@link com.tle.web.mimetypes.section.MimeDefaultViewerSection} + * + * @param attributes Configured attributes of MIME type + * @return A string representing a list of enabled viewers and formatted as a JSON array of + * strings (e.g. ["fancy", "toimg"]) + */ + String getEnabledViewerList(Map attributes); + interface MimeEntryChanges { void editMimeEntry(MimeEntry entry); } diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeServiceImpl.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeServiceImpl.java index 8b446944f1..1c988baaf6 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeServiceImpl.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/MimeTypeServiceImpl.java @@ -24,6 +24,7 @@ import com.google.common.cache.CacheLoader; import com.tle.annotation.Nullable; import com.tle.beans.Institution; +import com.tle.beans.item.ItemId; import com.tle.beans.item.attachments.Attachment; import com.tle.beans.item.attachments.AttachmentType; import com.tle.beans.item.attachments.CustomAttachment; @@ -38,7 +39,7 @@ import com.tle.core.guice.Bind; import com.tle.core.institution.InstitutionCache; import com.tle.core.institution.InstitutionService; -import com.tle.core.item.dao.AttachmentDao; +import com.tle.core.item.service.ItemService; import com.tle.core.mimetypes.dao.MimeEntryDao; import com.tle.core.mimetypes.institution.MimeMigrator; import com.tle.core.plugins.AbstractPluginService; @@ -76,7 +77,7 @@ public class MimeTypeServiceImpl implements MimeTypeService, MimeTypesUpdatedLis AbstractPluginService.getMyPluginId(MimeTypeServiceImpl.class) + "."; @Inject private MimeEntryDao mimeEntryDao; @Inject private EventService eventService; - @Inject private AttachmentDao attachmentDao; + @Inject private ItemService itemService; private PluginTracker textExtracterTracker; @@ -136,13 +137,6 @@ public void resetEntries() { @Override public MimeTypesSearchResults searchByMimeType(String mimeType, int offset, int length) { String query = (Check.isEmpty(mimeType) ? "" : mimeType.toLowerCase()); // $NON-NLS-1$ - /* - * Map typeMap = - * mimeCache.getCache().getMimeEntries(); List mimes = new - * ArrayList(); for( Entry entry : - * typeMap.entrySet() ) { if( entry.getKey().startsWith(query) ) { - * mimes.add(entry.getValue()); } } return mimes; - */ return mimeEntryDao.searchAll(query, offset, length); } @@ -174,8 +168,8 @@ public String getMimeTypeForFilename(String filename) { return DEFAULT_MIMETYPE; } - public String getMimeTypeForAttachmentUuid(String attachmentUuid) { - Attachment attachment = attachmentDao.findByUuid(attachmentUuid); + public String getMimeTypeForAttachmentUuid(ItemId key, String attachmentUuid) { + Attachment attachment = itemService.getAttachmentForUuid(key, attachmentUuid); return getMimeEntryForAttachment(attachment); } @@ -183,7 +177,10 @@ public String getMimeTypeForResourceAttachmentBean( ResourceAttachmentBean resourceAttachmentBean) { switch (resourceAttachmentBean.getResourceType()) { case SelectedResource.TYPE_ATTACHMENT: - return getMimeTypeForAttachmentUuid(resourceAttachmentBean.getAttachmentUuid()); + return getMimeTypeForAttachmentUuid( + new ItemId( + resourceAttachmentBean.getItemUuid(), resourceAttachmentBean.getItemVersion()), + resourceAttachmentBean.getAttachmentUuid()); case SelectedResource.TYPE_PATH: return MIME_ITEM; default: @@ -393,45 +390,6 @@ public List getAllTextExtracters() { @SuppressWarnings("unchecked") @Override public List getTextExtractersForMimeEntry(final MimeEntry mimeEntry) { - // Collection extensions = textExtracterTracker - // .getExtensions(new Filter() - // { - // @Override - // public boolean include(Extension t) - // { - // Collection mimes = t.getParameters("mimeType"); //$NON-NLS-1$ - // for( Parameter mime : mimes ) - // { - // String m = mime.valueAsString(); - // int wildIndex = m.indexOf('*'); - // if( wildIndex >= 0 ) - // { - // String nonWild = m.substring(0, wildIndex); - // if( mimeType.startsWith(nonWild) ) - // { - // return true; - // } - // } - // else - // { - // if( mimeType.equals(m) ) - // { - // return true; - // } - // } - // } - // return false; - // } - // }); - // - // List extracters = new - // ArrayList(); - // for( Extension e : extensions ) - // { - // extracters.add(textExtracterTracker.getBeanByExtension(e)); - // } - // return extracters; - List extracters = getAllTextExtracters(); List filtered = new ArrayList(); for (TextExtracterExtension extracter : extracters) { @@ -475,12 +433,14 @@ public String getMimeEntryForAttachment(Attachment attachment) { .getData("type") .equals(Character.toString(SelectedResource.TYPE_ATTACHMENT))) { // Recurse to drill into the linked attachment, so we can use the correct viewer. - // If more than one attachment has the linked uuid, - // this is a zip or scorm package and we can let it fall through. - List attachmentList = attachmentDao.findAllByUuid(attachment.getUrl()); - if (attachmentList.size() == 1) { - return getMimeEntryForAttachment(attachmentList.get(0)); - } + // data stored in getData("uuid") and getData("version") for a resource attachment gives the + // child item, which we need to determine the attachment to recurse into. + ItemId childAttachmentItem = + new ItemId((String) attachment.getData("uuid"), (int) attachment.getData("version")); + + Attachment childAttachment = + itemService.getNullableAttachmentForUuid(childAttachmentItem, attachment.getUrl()); + return childAttachment == null ? null : getMimeEntryForAttachment(childAttachment); } Map> map = getExtensionMap(); List extensions = map.get(type); @@ -489,10 +449,32 @@ public String getMimeEntryForAttachment(Attachment attachment) { return attachmentResources.getBeanByExtension(extension).getMimeType(attachment); } } - return null; } + @Override + public String getEnabledViewerList(Map attributes) { + String defaultViewer = attributes.get(MimeTypeConstants.KEY_DEFAULT_VIEWERID); + String enabledViewers = attributes.get(MimeTypeConstants.KEY_ENABLED_VIEWERS); + // File viewer is handled differently in 'MimeDefaultViewerSection' so there is no need to + // add it to the list. + if (Check.isEmpty(defaultViewer) + || defaultViewer.equals(MimeTypeConstants.VAL_DEFAULT_VIEWERID)) { + return enabledViewers; + } + + List enabledViewerList = new ArrayList<>(); + if (!Check.isEmpty(enabledViewers)) { + enabledViewerList.addAll( + JSONArray.toCollection(JSONArray.fromObject(enabledViewers), String.class)); + } + if (!enabledViewerList.contains(defaultViewer)) { + enabledViewerList.add(defaultViewer); + } + + return JSONArray.fromObject(enabledViewerList).toString(); + } + private synchronized Map> getExtensionMap() { if (extensionMap == null) { extensionMap = new HashMap>(); diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/institution/MimeEntryConverter.java b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/institution/MimeEntryConverter.java index 9a69af94f0..36e2e0d7d9 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/institution/MimeEntryConverter.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/core/mimetypes/institution/MimeEntryConverter.java @@ -27,9 +27,10 @@ import com.tle.core.hibernate.equella.service.InitialiserService; import com.tle.core.institution.convert.AbstractMigratableConverter; import com.tle.core.institution.convert.ConverterParams; +import com.tle.core.mimetypes.MimeTypeConstants; +import com.tle.core.mimetypes.MimeTypeService; import com.tle.core.mimetypes.dao.MimeEntryDao; import java.io.IOException; -import java.util.Iterator; import java.util.List; import java.util.Map; import javax.inject.Inject; @@ -43,6 +44,7 @@ public class MimeEntryConverter extends AbstractMigratableConverter { @Inject private MimeEntryDao mimeDao; @Inject private InitialiserService initialiserService; + @Inject private MimeTypeService mimeTypeService; @Override public void doDelete(Institution institution, ConverterParams callback) { @@ -102,15 +104,14 @@ public void importIt( @Override public void run() { MimeEntry mimeEntry = xmlHelper.readXmlFile(mimeFolder, entry); - Map attrs = mimeEntry.getAttributes(); - Iterator iter = attrs.values().iterator(); - while (iter.hasNext()) { - if (Check.isEmpty(iter.next())) { - iter.remove(); - } + String enabledViewers = mimeTypeService.getEnabledViewerList(attrs); + if (!Check.isEmpty(enabledViewers)) { + attrs.put(MimeTypeConstants.KEY_ENABLED_VIEWERS, enabledViewers); } + + attrs.values().removeIf(Check::isEmpty); mimeEntry.setInstitution(institution); mimeEntry.setId(0); diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeDefaultViewerSection.java b/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeDefaultViewerSection.java index 8c200a8daa..3cba80ac15 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeDefaultViewerSection.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeDefaultViewerSection.java @@ -225,6 +225,12 @@ public void loadEntry(SectionInfo info, MimeEntry entry) { } } + public boolean isDefaultViewerEnabled(SectionInfo info) { + String defaultViewerId = defaultViewer.getSelectedValueAsString(info); + Set enabledViewerList = enabledViewers.getSelectedValuesAsStrings(info); + return enabledViewerList.contains(defaultViewerId); + } + @Override public void saveEntry(SectionInfo info, MimeEntry entry) { String viewerId = defaultViewer.getSelectedValueAsString(info); @@ -285,6 +291,10 @@ public SectionResult renderHtml(RenderEventContext context) { HtmlBooleanState enabledState = enabledOption.getBooleanState(); CheckboxRenderer enabledCheck = new CheckboxRenderer(enabledState); + if (defaultOption.getBooleanState().isChecked()) { + enabledState.setChecked(true); + } + TextFieldRenderer configField = new TextFieldRenderer(viewerConfigs.getValueState(context, viewerId)); configField.setHidden(true); diff --git a/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeTypesEditSection.java b/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeTypesEditSection.java index 9d359d0af3..31ed861be9 100644 --- a/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeTypesEditSection.java +++ b/Source/Plugins/Core/com.equella.core/src/com/tle/web/mimetypes/section/MimeTypesEditSection.java @@ -67,7 +67,7 @@ @SuppressWarnings("nls") public class MimeTypesEditSection extends AbstractPrototypeSection implements HtmlRenderer { - private static PluginResourceHelper resources = + private static final PluginResourceHelper resources = ResourcesService.getResourceHelper(MimeTypesEditSection.class); public static final NameValue TAB_DETAILS = new BundleNameValue(resources.key("tab.details"), "details"); @@ -80,6 +80,9 @@ public class MimeTypesEditSection extends AbstractPrototypeSection + 94505 + c833659e-89e6-4693-bcee-cad05b49137e + 1 + TLE_ADMINISTRATOR + 2021-06-03 12:48:57.299 + 2021-06-03 12:48:57.301 + 2021-06-03 12:48:57.299 + -1.0 + false + LIVE + + + + 94511 + 9b5cec43-5283-41d2-b6f4-98c5bc2c1683 + source.gif + source.gif + 234932 + _THUMBS/source.gif.jpeg + 1511d04129a8d594ad2f6eace553e77f + false + false + false + + + + + + + 94508 + 2021-06-03 12:48:57.299 + 2021-06-03 12:48:57.299 + false + + + + + 94512 + TLE_ADMINISTRATOR + 2021-06-03 12:48:57.299 + false + contributed + DRAFT + + + 94513 + TLE_ADMINISTRATOR + 2021-06-03 12:48:57.299 + false + edit + DRAFT + + + 94514 + TLE_ADMINISTRATOR + 2021-06-03 12:48:57.299 + false + statechange + LIVE + + + + + + + + + 94509 + + + en_AU + + 94510 + en_AU + 2 + DeadAttachmentsTest + + + + + + + 94506 + + + 0 + + false + false + false + + default + \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/41/94505/_ITEM/item.xml b/autotest/Tests/tests/rest/institution/items/41/94505/_ITEM/item.xml new file mode 100644 index 0000000000..572bfa5d7c --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/41/94505/_ITEM/item.xml @@ -0,0 +1 @@ +DeadAttachmentsTest9b5cec43-5283-41d2-b6f4-98c5bc2c1683 \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/53/95797.xml b/autotest/Tests/tests/rest/institution/items/53/95797.xml new file mode 100644 index 0000000000..4e37be7b10 --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/53/95797.xml @@ -0,0 +1,102 @@ + + 95797 + 918a2381-a445-45f5-95b6-809e113c370d + 1 + TLE_ADMINISTRATOR + 2021-06-03 13:50:20.689 + 2021-06-03 13:50:20.69 + 2021-06-03 13:50:20.689 + -1.0 + false + LIVE + + + + 95802 + fcb5cd6f-16d1-4b75-a53f-436e21b1f23d + d3cd2079-a1f8-457c-b4ab-b77a0e87d8e0 + source.gif + resource + + + type + a + + + uuid + dda553d9-8029-488e-8bbc-90012a77935b + + + version + 1 + + + false + false + false + + + + + + + 95799 + 2021-06-03 13:50:20.689 + 2021-06-03 13:50:20.689 + false + + + + + 95803 + TLE_ADMINISTRATOR + 2021-06-03 13:50:20.689 + false + contributed + DRAFT + + + 95804 + TLE_ADMINISTRATOR + 2021-06-03 13:50:20.689 + false + edit + DRAFT + + + 95805 + TLE_ADMINISTRATOR + 2021-06-03 13:50:20.689 + false + statechange + LIVE + + + + + + + + + 95800 + + + en_AU + + 95801 + en_AU + 2 + NestedDeadResourceAttachmentTest - Child 1 + + + + + + 0 + + false + false + false + + default + \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/53/95797/_ITEM/item.xml b/autotest/Tests/tests/rest/institution/items/53/95797/_ITEM/item.xml new file mode 100644 index 0000000000..3b0b35e442 --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/53/95797/_ITEM/item.xml @@ -0,0 +1 @@ +fcb5cd6f-16d1-4b75-a53f-436e21b1f23dNestedDeadResourceAttachmentTest - Child 1 \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/63/95807.xml b/autotest/Tests/tests/rest/institution/items/63/95807.xml new file mode 100644 index 0000000000..907a10476b --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/63/95807.xml @@ -0,0 +1,102 @@ + + 95807 + a101af01-e13e-4dd3-bb1d-117ae5a92745 + 1 + TLE_ADMINISTRATOR + 2021-06-03 13:50:48.447 + 2021-06-03 13:50:48.448 + 2021-06-03 13:50:48.447 + -1.0 + false + LIVE + + + + 95812 + d185b602-020f-4c64-929d-5524f1b6e21d + fcb5cd6f-16d1-4b75-a53f-436e21b1f23d + source.gif + resource + + + type + a + + + uuid + 918a2381-a445-45f5-95b6-809e113c370d + + + version + 1 + + + false + false + false + + + + + + + 95809 + 2021-06-03 13:50:48.447 + 2021-06-03 13:50:48.447 + false + + + + + 95813 + TLE_ADMINISTRATOR + 2021-06-03 13:50:48.447 + false + contributed + DRAFT + + + 95814 + TLE_ADMINISTRATOR + 2021-06-03 13:50:48.447 + false + edit + DRAFT + + + 95815 + TLE_ADMINISTRATOR + 2021-06-03 13:50:48.447 + false + statechange + LIVE + + + + + + + + + 95810 + + + en_AU + + 95811 + en_AU + 2 + NestedDeadResourceAttachmentTest - Child 2 + + + + + + 0 + + false + false + false + + default + \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/63/95807/_ITEM/item.xml b/autotest/Tests/tests/rest/institution/items/63/95807/_ITEM/item.xml new file mode 100644 index 0000000000..50ae7d0091 --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/63/95807/_ITEM/item.xml @@ -0,0 +1 @@ +d185b602-020f-4c64-929d-5524f1b6e21dNestedDeadResourceAttachmentTest - Child 2 \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/73/95817.xml b/autotest/Tests/tests/rest/institution/items/73/95817.xml new file mode 100644 index 0000000000..320a7a2f0d --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/73/95817.xml @@ -0,0 +1,102 @@ + + 95817 + c824b750-7dd6-493d-8ba0-c6570615d31b + 1 + TLE_ADMINISTRATOR + 2021-06-03 13:51:40.084 + 2021-06-03 13:51:40.086 + 2021-06-03 13:51:40.084 + -1.0 + false + LIVE + + + + 95822 + 311c3a7a-f4a2-4b21-b7a2-cdfc2789db2d + + NestedDeadResourceAttachmentTest - Root + resource + + + type + p + + + uuid + dda553d9-8029-488e-8bbc-90012a77935b + + + version + 1 + + + false + false + false + + + + + + + 95819 + 2021-06-03 13:51:40.084 + 2021-06-03 13:51:40.084 + false + + + + + 95823 + TLE_ADMINISTRATOR + 2021-06-03 13:51:40.084 + false + contributed + DRAFT + + + 95824 + TLE_ADMINISTRATOR + 2021-06-03 13:51:40.084 + false + edit + DRAFT + + + 95825 + TLE_ADMINISTRATOR + 2021-06-03 13:51:40.084 + false + statechange + LIVE + + + + + + + + + 95820 + + + en_AU + + 95821 + en_AU + 2 + NestedDeadResourceAttachmentTest - Points at root item summary + + + + + + 0 + + false + false + false + + default + \ No newline at end of file diff --git a/autotest/Tests/tests/rest/institution/items/73/95817/_ITEM/item.xml b/autotest/Tests/tests/rest/institution/items/73/95817/_ITEM/item.xml new file mode 100644 index 0000000000..ac334440e8 --- /dev/null +++ b/autotest/Tests/tests/rest/institution/items/73/95817/_ITEM/item.xml @@ -0,0 +1 @@ +311c3a7a-f4a2-4b21-b7a2-cdfc2789db2dNestedDeadResourceAttachmentTest - Points at root item summary \ No newline at end of file diff --git a/build.sbt b/build.sbt index c35e4eef9a..e7814f534f 100644 --- a/build.sbt +++ b/build.sbt @@ -116,7 +116,7 @@ name := "Equella" equellaMajor in ThisBuild := 2021 equellaMinor in ThisBuild := 1 -equellaPatch in ThisBuild := 0 +equellaPatch in ThisBuild := 1 equellaStream in ThisBuild := "Stable" equellaBuild in ThisBuild := buildConfig.value.getString("build.buildname") diff --git a/buildspec.yml b/buildspec.yml index 64ee44cc82..4a34717788 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -2,8 +2,7 @@ version: 0.2 env: variables: - # TODO [SpringHib5] keep this in sync with the appropriate oEQ-Kaltura branch before merging into `develop`, and eventually into `master`. - KALTURA_BRANCH: "develop" + KALTURA_BRANCH: "master" AUTOTEST_CONFIG: "autotest/codebuild.conf" EQ_EXIFTOOL_PATH: "/usr/bin/exiftool" OLD_TEST_NEWUI: true @@ -20,6 +19,7 @@ phases: build: commands: - (npm ci && cd Source/Plugins/Core/com.equella.core/js && npm ci) + - (cd Source/Plugins/Kaltura/com.tle.web.wizard.controls.kaltura/js && npm ci && npm run build) - (npm run check:ts && npm run check:ts-types-source && npm run check:license) - (cd Source/Plugins/Core/com.equella.core/js && npm test) - sbt -no-colors -Dconfig.file=${HOME}/build.conf test installerZip writeLanguagePack writeScriptingJavadoc diff --git a/oeq-ts-rest-api/src/MimeType.ts b/oeq-ts-rest-api/src/MimeType.ts index 151570af3d..00e8390477 100644 --- a/oeq-ts-rest-api/src/MimeType.ts +++ b/oeq-ts-rest-api/src/MimeType.ts @@ -71,6 +71,7 @@ export type ViewerId = | 'externalToolViewer' | 'fancy' | 'file' + | 'flvViewer' | 'googledocviewer' | 'htmlFiveViewer' | 'kalturaViewer' diff --git a/oeq-ts-rest-api/src/Search.ts b/oeq-ts-rest-api/src/Search.ts index aa796c8250..d296e5762f 100644 --- a/oeq-ts-rest-api/src/Search.ts +++ b/oeq-ts-rest-api/src/Search.ts @@ -181,6 +181,11 @@ export interface Attachment { * The description of an attachment. */ description?: string; + + /** + * Whether or not the attachment has been determined to be broken by the server. + */ + brokenAttachment: boolean; /** * True if an attachment can be previewed. */ diff --git a/oeq-ts-rest-api/test/Search.test.ts b/oeq-ts-rest-api/test/Search.test.ts index 956983f17e..7227799c71 100644 --- a/oeq-ts-rest-api/test/Search.test.ts +++ b/oeq-ts-rest-api/test/Search.test.ts @@ -164,3 +164,68 @@ describe('Exports search results for the specified search params', function () { ).resolves.toBe(true); }); }); + +describe('Dead attachment handling', () => { + it.each<[string, string, number, number, boolean]>([ + [ + 'an intact file attachment as not broken', + 'Keyword found in attachment test item', + 0, + 0, + false, + ], + [ + 'an intact resource selector attachment as not broken', + 'ItemApiViewTest - All attachments', + 0, + 1, + false, + ], + [ + 'a returned file attachment missing from the filestore as broken', + 'DeadAttachmentsTest', + 0, + 0, + true, + ], + [ + 'a resource attachment pointing at a non-existent attachment as broken', + 'NestedDeadResourceAttachmentTest - Child 1', + 0, + 0, + true, + ], + [ + 'a resource attachment pointing at a non-existent item summary as broken', + 'NestedDeadResourceAttachmentTest - Points at root item summary', + 0, + 0, + true, + ], + [ + 'a nested resource attachment with the end of the chain being a non-existent attachment as broken', + 'NestedDeadResourceAttachmentTest - Child 2', + 0, + 0, + true, + ], + ])( + 'should mark %s', + async ( + _: string, + query: string, + itemResultIndex: number, + attachmentResultIndex: number, + expectBrokenStatus: boolean + ) => { + const searchResult = await doSearch({ query: query }); + const { attachments } = searchResult.results[itemResultIndex]; + if (attachments === undefined) { + throw new Error('Unexpected undefined attachments'); + } + const brokenAttachment = + attachments[attachmentResultIndex].brokenAttachment; + expect(brokenAttachment).toEqual(expectBrokenStatus); + } + ); +});