mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-07 17:34:21 -04:00
feat: LTI support and Moodle plugin
This commit is contained in:
@@ -45,12 +45,14 @@ export const LayoutProvider = ({ children }) => {
|
||||
const site = useContext(SiteContext);
|
||||
const cache = new BrowserCache('MediaCMS[' + site.id + '][layout]', 86400);
|
||||
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media', []);
|
||||
const isMediaPage = useMemo(() => PageStore.get('current-page') === 'media' || window.MediaCMS?.mediaId !== undefined, []);
|
||||
const isEmbeddedApp = useMemo(() => inEmbeddedApp(), []);
|
||||
|
||||
const enabledSidebar = Boolean(document.getElementById('app-sidebar') || document.querySelector('.page-sidebar'));
|
||||
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(cache.get('visible-sidebar'));
|
||||
const [visibleSidebar, setVisibleSidebar] = useState(
|
||||
isMediaPage || isEmbeddedApp ? false : cache.get('visible-sidebar')
|
||||
);
|
||||
const [visibleMobileSearch, setVisibleMobileSearch] = useState(false);
|
||||
|
||||
const toggleMobileSearch = () => {
|
||||
|
||||
@@ -3,18 +3,100 @@ export function inEmbeddedApp() {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const mode = params.get('mode');
|
||||
|
||||
if (mode === 'embed_mode') {
|
||||
sessionStorage.setItem('media_cms_embed_mode', 'true');
|
||||
if (mode === 'lms_embed_mode') {
|
||||
sessionStorage.setItem('lms_embed_mode', 'true');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (mode === 'standard') {
|
||||
sessionStorage.removeItem('media_cms_embed_mode');
|
||||
sessionStorage.removeItem('lms_embed_mode');
|
||||
return false;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('media_cms_embed_mode') === 'true';
|
||||
return sessionStorage.getItem('lms_embed_mode') === 'true';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isShareMediaDisabled(): boolean {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const shareMedia = params.get('share_media');
|
||||
const mode = params.get('mode');
|
||||
|
||||
if (shareMedia === '0') {
|
||||
sessionStorage.setItem('lms_share_media_disabled', 'true');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fresh LTI landing (mode=lms_embed_mode in URL) without share_media=0
|
||||
// means sharing is enabled — clear any stale disabled flag.
|
||||
if (shareMedia === '1' || mode === 'lms_embed_mode') {
|
||||
sessionStorage.removeItem('lms_share_media_disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('lms_share_media_disabled') === 'true';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isSelectMediaMode() {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const action = params.get('action');
|
||||
|
||||
return action === 'select_media';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function inSelectMediaEmbedMode() {
|
||||
return inEmbeddedApp() && isSelectMediaMode();
|
||||
}
|
||||
|
||||
// When MediaCMS is embedded inside a host platform (e.g. an LMS), the host passes a
|
||||
// `parent_media_base` URL via LTI custom params so that media title links in the embed
|
||||
// player navigate the parent frame to the host's own media viewer (e.g. Moodle My Media)
|
||||
// instead of opening a bare MediaCMS URL. The VideoViewer appends `?token=<friendly_token>`
|
||||
// and uses `target="_parent"` to perform the navigation.
|
||||
export function getParentMediaBase(): string | null {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const mode = params.get('mode');
|
||||
const base = params.get('parent_media_base');
|
||||
|
||||
if (mode === 'standard') {
|
||||
sessionStorage.removeItem('parent_media_base');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (base) {
|
||||
sessionStorage.setItem('parent_media_base', base);
|
||||
return base;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('parent_media_base');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLtiContextId(): string | null {
|
||||
try {
|
||||
const params = new URL(globalThis.location.href).searchParams;
|
||||
const contextId = params.get('lti_context_id');
|
||||
|
||||
if (contextId) {
|
||||
sessionStorage.setItem('lti_context_id', contextId);
|
||||
return contextId;
|
||||
}
|
||||
|
||||
return sessionStorage.getItem('lti_context_id');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ export * from './quickSort';
|
||||
export * from './requests';
|
||||
export { translateString } from './translate';
|
||||
export { replaceString } from './replacementStrings';
|
||||
export * from './embeddedApp';
|
||||
export { getParentMediaBase, inEmbeddedApp, inSelectMediaEmbedMode, isSelectMediaMode, isShareMediaDisabled } from './embeddedApp';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// check templates/config/installation/translations.html for more
|
||||
|
||||
export function translateString(str) {
|
||||
return window.TRANSLATION?.[str] ?? str;
|
||||
return window.TRANSLATION?.[str] || str;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { translateString } from '../helpers';
|
||||
import { inEmbeddedApp } from '../helpers/embeddedApp';
|
||||
|
||||
/**
|
||||
* Custom hook for managing bulk actions on media items
|
||||
@@ -22,6 +23,20 @@ export function useBulkActions() {
|
||||
const [showPublishStateModal, setShowPublishStateModal] = useState(false);
|
||||
const [showCategoryModal, setShowCategoryModal] = useState(false);
|
||||
const [showTagModal, setShowTagModal] = useState(false);
|
||||
const [showCourseCleanupModal, setShowCourseCleanupModal] = useState(false);
|
||||
const [hasContributorCourses, setHasContributorCourses] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inEmbeddedApp()) return;
|
||||
fetch('/api/v1/categories/contributor?lms_courses_only=true')
|
||||
.then((r) => r.ok ? r.json() : null)
|
||||
.then((data) => {
|
||||
if (!data) return;
|
||||
const courses = data.results || data;
|
||||
setHasContributorCourses(Array.isArray(courses) && courses.length > 0);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Get CSRF token from cookies
|
||||
const getCsrfToken = () => {
|
||||
@@ -95,6 +110,11 @@ export function useBulkActions() {
|
||||
const handleBulkAction = (action) => {
|
||||
const selectedCount = selectedMedia.size;
|
||||
|
||||
if (action === 'course-cleanup') {
|
||||
setShowCourseCleanupModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCount === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -111,6 +131,10 @@ export function useBulkActions() {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
setConfirmMessage(translateString('You are going to disable comments to') + ` ${selectedCount} ` + translateString('media, are you sure?'));
|
||||
} else if (action === 'delete-comments') {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
setConfirmMessage(translateString('You are going to delete all comments from') + ` ${selectedCount} ` + translateString('media, are you sure?'));
|
||||
} else if (action === 'enable-download') {
|
||||
setShowConfirmModal(true);
|
||||
setPendingAction(action);
|
||||
@@ -165,6 +189,8 @@ export function useBulkActions() {
|
||||
executeEnableComments();
|
||||
} else if (action === 'disable-comments') {
|
||||
executeDisableComments();
|
||||
} else if (action === 'delete-comments') {
|
||||
executeDeleteComments();
|
||||
} else if (action === 'enable-download') {
|
||||
executeEnableDownload();
|
||||
} else if (action === 'disable-download') {
|
||||
@@ -271,6 +297,37 @@ export function useBulkActions() {
|
||||
});
|
||||
};
|
||||
|
||||
// Execute delete comments
|
||||
const executeDeleteComments = () => {
|
||||
const selectedIds = Array.from(selectedMedia);
|
||||
|
||||
fetch('/api/v1/media/user/bulk_actions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'delete_comments',
|
||||
media_ids: selectedIds,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete comments');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
showNotificationMessage(translateString('Successfully deleted comments'));
|
||||
clearSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotificationMessage(translateString('Failed to delete comments.'), 'error');
|
||||
clearSelection();
|
||||
});
|
||||
};
|
||||
|
||||
// Execute enable download
|
||||
const executeEnableDownload = () => {
|
||||
const selectedIds = Array.from(selectedMedia);
|
||||
@@ -463,6 +520,22 @@ export function useBulkActions() {
|
||||
setShowTagModal(false);
|
||||
};
|
||||
|
||||
// Course cleanup modal handlers
|
||||
const handleCourseCleanupModalCancel = () => {
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
const handleCourseCleanupModalSuccess = (message) => {
|
||||
showNotificationMessage(message);
|
||||
clearSelectionAndRefresh();
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
const handleCourseCleanupModalError = (message) => {
|
||||
showNotificationMessage(message, 'error');
|
||||
setShowCourseCleanupModal(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedMedia,
|
||||
@@ -480,6 +553,8 @@ export function useBulkActions() {
|
||||
showPublishStateModal,
|
||||
showCategoryModal,
|
||||
showTagModal,
|
||||
showCourseCleanupModal,
|
||||
hasContributorCourses,
|
||||
|
||||
// Handlers
|
||||
handleMediaSelection,
|
||||
@@ -507,6 +582,9 @@ export function useBulkActions() {
|
||||
handleTagModalCancel,
|
||||
handleTagModalSuccess,
|
||||
handleTagModalError,
|
||||
handleCourseCleanupModalCancel,
|
||||
handleCourseCleanupModalSuccess,
|
||||
handleCourseCleanupModalError,
|
||||
|
||||
// Utility
|
||||
getCsrfToken,
|
||||
|
||||
@@ -195,13 +195,18 @@ class MediaPageStore extends EventEmitter {
|
||||
this.emit('loaded_media_data');
|
||||
}
|
||||
|
||||
this.loadPlaylists();
|
||||
if (MediaCMS.features.media.actions.comment_mention === true) {
|
||||
this.loadUsers();
|
||||
}
|
||||
// Skip loading playlists and comments when in embed mode (to reduce API calls)
|
||||
const isEmbedMode = window.location.pathname.startsWith('/embed');
|
||||
|
||||
if (this.mediacms_config.member.can.readComment) {
|
||||
this.loadComments();
|
||||
if (!isEmbedMode) {
|
||||
this.loadPlaylists();
|
||||
if (MediaCMS.features.media.actions.comment_mention === true) {
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
if (this.mediacms_config.member.can.readComment) {
|
||||
this.loadComments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -882,7 +887,7 @@ class MediaPageStore extends EventEmitter {
|
||||
|
||||
submitCommentResponse(response) {
|
||||
if (response && 201 === response.status && response.data && Object.keys(response.data)) {
|
||||
MediaPageStoreData[this.id].comments.push(response.data);
|
||||
MediaPageStoreData[this.id].comments.unshift(response.data);
|
||||
this.emit('comment_submit', response.data.uid);
|
||||
}
|
||||
setTimeout(
|
||||
|
||||
Reference in New Issue
Block a user