Skip to content

Commit

Permalink
Merge branch 'Simon-Initiative:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
dtiwarATS committed Feb 22, 2024
2 parents d2c6696 + f27dd29 commit 016ebb3
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 42 deletions.
150 changes: 110 additions & 40 deletions assets/src/components/media/manager/MediaManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Immutable from 'immutable';
import { Maybe } from 'tsmonad';
import { formatDate } from 'components/activities/common/utils';
import { LoadingSpinner, LoadingSpinnerSize } from 'components/common/LoadingSpinner';
import ConfirmDelete from 'apps/authoring/components/Modal/DeleteConfirmationModal';
import { MediaItem } from 'types/media';
import { classNames } from 'utils/classNames';
import { relativeToNow } from 'utils/date';
Expand All @@ -13,6 +14,7 @@ import { OrderedMediaLibrary } from '../OrderedMediaLibrary';
import { MediaIcon } from './MediaIcon';
import './MediaManager.scss';
import { VideoUploadWarning } from './VideoUploadWarning';
import { deleteFiles } from './delete';
import { uploadFiles } from './upload';

const PAGELOAD_TRIGGER_MARGIN_PX = 100;
Expand Down Expand Up @@ -126,6 +128,7 @@ export interface MediaManagerState {
filteredMimeTypes: string[] | undefined;
uploading: boolean;
duplicateWarning: string[];
showConfirmDelete: boolean;
}

const setMediaManagerLayoutSetting = (layout: LAYOUTS) => {
Expand Down Expand Up @@ -164,6 +167,7 @@ export class MediaManager extends React.PureComponent<MediaManagerProps, MediaMa
filteredMimeTypes: props.mimeFilter,
uploading: false,
duplicateWarning: [],
showConfirmDelete: false,
};

this.onScroll = this.onScroll.bind(this);
Expand Down Expand Up @@ -229,6 +233,35 @@ export class MediaManager extends React.PureComponent<MediaManagerProps, MediaMa
(window as any).$('#' + id).trigger('click');
}

onDelete() {
const { mimeFilter, onLoadCourseMediaNextPage, onResetMedia } = this.props;
const { searchText, orderBy, order, selection } = this.state;

deleteFiles(this.props.projectSlug, selection.toArray())
.then((result: any) => {
onResetMedia();
onLoadCourseMediaNextPage(
this.props.projectSlug,
mimeFilter as string[],
searchText as string,
orderBy,
order,
);
this.setState({ selection: Immutable.List<string>() });
})
.catch((e: Error | string) => {
if (typeof e === 'string') {
this.setState({ error: Maybe.just(e) });
} else {
this.setState({ error: Maybe.just(e.message) });
}
});
}

setShowConfirmDelete(showConfirmDelete: boolean) {
this.setState({ showConfirmDelete });
}

setupScrollListener(scrollView: HTMLElement) {
if (!scrollView) {
return;
Expand Down Expand Up @@ -530,16 +563,27 @@ export class MediaManager extends React.PureComponent<MediaManagerProps, MediaMa

renderMediaSelectionDetails(disabled: boolean) {
const { media } = this.props;
const { selection, showDetails } = this.state;
const { selection, showDetails, showConfirmDelete } = this.state;

const selectedMediaItems = selection
.map((guid) => media.data.get(guid))
.filter((item) => !!item) as Immutable.List<MediaItem>;

if (selectedMediaItems.size > 1) {
return (
<div className="media-selection-details">
<div className="details-title">Multiple Items Selected</div>
<div className="flex">
<div className="media-selection-details flex-grow-1">
<div className="details-title">Multiple Items Selected</div>
</div>
<div>
<button
className="btn p-0 ml-1"
onClick={() => this.setShowConfirmDelete(true)}
title="Archive Media"
>
<i className="fa fa-trash" />
</button>
</div>
</div>
);
}
Expand All @@ -550,46 +594,72 @@ export class MediaManager extends React.PureComponent<MediaManagerProps, MediaMa
const selectedItem = selectedMediaItems.first() as MediaItem;

return (
<div className="media-selection-details">
<div className="details-title">
<span>
Selected:{' '}
<a
href={selectedItem.url}
rel="noreferrer"
target="_blank"
onClick={(e) => {
if (!disabled) {
popOpenImage(e);
}
<div className="flex">
<div className="media-selection-details flex-grow-1">
<div className="details-title">
<span>
Selected:{' '}
<a
href={selectedItem.url}
rel="noreferrer"
target="_blank"
onClick={(e) => {
if (!disabled) {
popOpenImage(e);
}
}}
>
{stringFormat.ellipsize(selectedItem.fileName, 65, 5)}
</a>
</span>
{showDetails ? (
<i className="fa-solid fa-angle-down"></i>
) : (
<i className="fa-solid fa-angle-up"></i>
)}
</div>
{showDetails && (
<div className="details-content">
<MediaIcon
filename={selectedItem.fileName}
mimeType={selectedItem.mimeType}
url={selectedItem.url}
/>
<div className="details-info">
<div className="detail-row date-created">
<b>Uploaded:</b> {relativeToNow(new Date(selectedItem.dateCreated))}
</div>
<div className="detail-row file-size">
<b>Size:</b> {convert.toByteNotation(selectedItem.fileSize)}
</div>
</div>
</div>
)}
{showConfirmDelete && (
<ConfirmDelete
show={showConfirmDelete}
title="Archive Media"
elementType="Media"
explanation="Are you sure you want to archive the file(s)? Note that archived media items are no longer available in the media library and the archiving process is not reversible"
deleteHandler={() => {
this.onDelete();
this.setShowConfirmDelete(false);
}}
>
{stringFormat.ellipsize(selectedItem.fileName, 65, 5)}
</a>
</span>
{showDetails ? (
<i className="fa-solid fa-angle-down"></i>
) : (
<i className="fa-solid fa-angle-up"></i>
cancelHandler={() => {
this.setShowConfirmDelete(false);
}}
/>
)}
</div>
{showDetails && (
<div className="details-content">
<MediaIcon
filename={selectedItem.fileName}
mimeType={selectedItem.mimeType}
url={selectedItem.url}
/>
<div className="details-info">
<div className="detail-row date-created">
<b>Uploaded:</b> {relativeToNow(new Date(selectedItem.dateCreated))}
</div>
<div className="detail-row file-size">
<b>Size:</b> {convert.toByteNotation(selectedItem.fileSize)}
</div>
</div>
</div>
)}
<div>
<button
className="btn p-0 ml-1"
onClick={() => this.setShowConfirmDelete(true)}
title="Archive Media"
>
<i className="fa fa-trash" />
</button>
</div>
</div>
);
}
Expand Down
7 changes: 7 additions & 0 deletions assets/src/components/media/manager/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as persistence from 'data/persistence/media';

export const deleteFiles = async (projectSlug: string, files: string[]) => {
return persistence.deleteMedia(projectSlug, files).then((result) => {
return result;
});
};
18 changes: 18 additions & 0 deletions assets/src/data/persistence/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export type MediaItemCreated = {
url: string;
};

export type MediaItemsDeleted = {
type: 'success';
count: number;
};

export function getFileName(file: File) {
const fileNameWithDot = file.name.slice(
0,
Expand Down Expand Up @@ -65,6 +70,19 @@ export function createMedia(
});
}

export function deleteMedia(
project: ProjectSlug,
mediaIds: string[],
): Promise<MediaItemsDeleted | ServerError> {
const params = {
method: 'POST',
url: `/media/project/${project}/delete`,
body: JSON.stringify({ mediaItemIds: mediaIds }),
};

return makeRequest<MediaItemsDeleted>(params);
}

export function fetchMedia(
project: ProjectSlug,
offset?: number,
Expand Down
34 changes: 33 additions & 1 deletion lib/oli/authoring/media_library.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,45 @@ defmodule Oli.Authoring.MediaLibrary do
|> insert(project.id, file_name, file_contents, hash)

{:duplicate_content, item} ->
{:duplicate, item}
if item.deleted do
restore_item(item.id)
{:ok, item}
else
{:duplicate, item}
end
end
else
{:error, {:not_found}}
end
end

@spec delete_media_items(String.t(), any) :: {:ok, any} | {:error, any}
def delete_media_items(project_slug, media_ids) do
project = Oli.Authoring.Course.get_project_by_slug(project_slug)

if project != nil do
case delete_items(media_ids) do
{changes_count, nil} when is_integer(changes_count) ->
{:ok, changes_count}

error ->
{:error, error}
end
else
{:error, {:not_found}}
end
end

def delete_items(media_ids) do
from(m in MediaItem, where: m.id in ^media_ids)
|> Repo.update_all(set: [deleted: true])
end

def restore_item(media_id) do
from(m in MediaItem, where: m.id == ^media_id)
|> Repo.update_all(set: [deleted: false])
end

@doc """
Access the items in a project's media library with support for paging
and filtering.
Expand Down
66 changes: 65 additions & 1 deletion lib/oli_web/controllers/api/media_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule OliWeb.Api.MediaController do
use OliWeb, :controller
use OpenApiSpex.Controller

plug(:fetch_project when action not in [:index, :create])
plug(:fetch_project when action not in [:index, :create, :delete])

defmodule MediaItemUpload do
require OpenApiSpex
Expand All @@ -36,6 +36,41 @@ defmodule OliWeb.Api.MediaController do
})
end

defmodule MediaItemsDelete do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "Media Items Delete",
description: "Deleted media items",
type: :object,
properties: %{
mediaItemIds: %Schema{type: :list, description: "The media items ids"}
},
required: [:mediaItemIds],
example: %{
"mediaItemIds" => [1, 2, 3]
}
})
end

defmodule MediaItemsDeleteResponse do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "Media Items Delete Response",
description: "Deleted media items response",
type: :object,
properties: %{
type: %Schema{type: :string, description: "Success"},
count: %Schema{type: :integer, description: "Count of media deleted"}
},
required: [:result],
example: %{
"result" => "success"
}
})
end

defmodule MediaItemUploadResponse do
require OpenApiSpex

Expand Down Expand Up @@ -231,6 +266,35 @@ defmodule OliWeb.Api.MediaController do
end
end

@doc """
Create a new media library entry by uploading the encoded media file to an Amazon S3 storage bucket.
"""
@doc parameters: [
project: [
in: :url,
schema: %OpenApiSpex.Schema{type: :string},
required: true,
description: "The project id"
]
],
request_body:
{"Request body to delete a list of media items", "application/json",
OliWeb.Api.MediaController.MediaItemsDelete, required: true},
responses: %{
200 =>
{"Media Items Delete Response", "application/json",
OliWeb.Api.MediaController.MediaItemsDeleteResponse}
}
def delete(conn, %{"project" => project_slug, "mediaItemIds" => media_item_ids}) do
case MediaLibrary.delete_media_items(project_slug, media_item_ids) do
{:ok, count} ->
json(conn, %{type: "success", count: count})

{:error, err_msg} ->
error(conn, 400, err_msg)
end
end

defp error(conn, code, reason) do
conn
|> send_resp(code, prettify_error(reason))
Expand Down
1 change: 1 addition & 0 deletions lib/oli_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ defmodule OliWeb.Router do
pipe_through([:api, :authoring_protected])

post("/", Api.MediaController, :create)
post("/delete", Api.MediaController, :delete)
get("/", Api.MediaController, :index)
end

Expand Down
Loading

0 comments on commit 016ebb3

Please sign in to comment.