diff --git a/SECURITY.md b/SECURITY.md index ed83fbf7e..a27e766d9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ Use the application at your own risk. People who have worked on this project wil ## Reporting a vulnerability -Please drop a mail at [hi@sub.rajatsaxena.dev](mailto:hi@sub.rajatsaxena.dev) or tweet at [@CourseLit](https://twitter.com/courselit). We will try to address it as soon as possible. +Please drop a mail at [hi@codelit.dev](mailto:hi@codelit.dev) or tweet at [@CourseLit](https://twitter.com/courselit). We will try to address it as soon as possible. diff --git a/apps/docs/src/pages/en/users/permissions.md b/apps/docs/src/pages/en/users/permissions.md index 578b9134f..a5ad87805 100644 --- a/apps/docs/src/pages/en/users/permissions.md +++ b/apps/docs/src/pages/en/users/permissions.md @@ -44,22 +44,10 @@ Let take a moment to understand what all permissions are available and what aspe Access/update school's users. -- **View files** - - Access school's files like images, videos, PDFs etc. - -- **Upload files** - - Upload media assets like images, videos, PDFs etc. - - **Manage files** Update/delete your media assets -- **Manage all files** - - Update/delete any media asset. This includes media uploaded by other creators in the school. - ## Stuck somewhere? We are always here for you. Come chat with us in our Discord channel or send a tweet at @CourseLit. diff --git a/apps/web/components/admin/blogs/editor/details.tsx b/apps/web/components/admin/blogs/editor/details.tsx index 17798f075..7f3680816 100644 --- a/apps/web/components/admin/blogs/editor/details.tsx +++ b/apps/web/components/admin/blogs/editor/details.tsx @@ -7,6 +7,7 @@ import { Form, FormField, Button, + PageBuilderPropertyHeader, } from "@courselit/components-library"; import useCourse from "./course-hook"; import { FetchBuilder } from "@courselit/utils"; @@ -18,6 +19,7 @@ import { Address, AppMessage, Auth, Profile } from "@courselit/common-models"; import { APP_MESSAGE_COURSE_SAVED, BUTTON_SAVE, + COURSE_CONTENT_HEADER, FORM_FIELD_FEATURED_IMAGE, } from "../../../../ui-config/strings"; import { connect } from "react-redux"; @@ -61,27 +63,7 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { updateCourse(courseData: { id: "${course!.id}" title: "${title}", - description: ${JSON.stringify(JSON.stringify(description))}, - featuredImage: ${ - featuredImage.mediaId - ? `{ - mediaId: "${featuredImage.mediaId}", - originalFileName: "${ - featuredImage.originalFileName - }", - mimeType: "${featuredImage.mimeType}", - size: ${featuredImage.size}, - access: "${featuredImage.access}", - file: ${ - featuredImage.access === "public" - ? `"${featuredImage.file}"` - : null - }, - thumbnail: "${featuredImage.thumbnail}", - caption: "${featuredImage.caption}" - }` - : null - } + description: ${JSON.stringify(JSON.stringify(description))} }) { id } @@ -107,22 +89,67 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { } }; + const saveFeaturedImage = async (media?: Media) => { + const mutation = ` + mutation ($courseId: ID!, $media: MediaInput) { + updateCourse(courseData: { + id: $courseId + featuredImage: $media + }) { + id + } + } + `; + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query: mutation, + variables: { + courseId: course?.id, + media: media || null, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + try { + dispatch(networkAction(true)); + const response = await fetch.exec(); + if (response.updateCourse) { + dispatch( + setAppMessage(new AppMessage(APP_MESSAGE_COURSE_SAVED)), + ); + } + } catch (err: any) { + dispatch(setAppMessage(new AppMessage(err.message))); + } finally { + dispatch(networkAction(false)); + } + }; + return ( -
-
- setTitle(e.target.value)} - /> - setDescription(state)} - url={address.backend} - /> +
+
+ + setTitle(e.target.value)} + /> + + setDescription(state)} + url={address.backend} + /> +
+ +
+ +
+
{ media && setFeaturedImage(media); + saveFeaturedImage(media); }} mimeTypesToShow={[...MIMETYPE_IMAGE]} access="public" @@ -142,13 +170,12 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { mediaId={(featuredImage && featuredImage.mediaId) || ""} onRemove={() => { setFeaturedImage({}); + saveFeaturedImage(); }} + type="course" /> -
- -
- -
+
+ ); } diff --git a/apps/web/components/admin/dashboard/to-do.tsx b/apps/web/components/admin/dashboard/to-do.tsx index d8b533e42..5a48ef7b2 100644 --- a/apps/web/components/admin/dashboard/to-do.tsx +++ b/apps/web/components/admin/dashboard/to-do.tsx @@ -17,7 +17,7 @@ const Todo = ({ siteinfo }) => { {(!siteinfo.title || (siteinfo.logo && !siteinfo.logo.file)) && (

- Basic details missing + Basic details missing 💁‍♀️

Give your school a proper name, description and a logo. @@ -35,7 +35,7 @@ const Todo = ({ siteinfo }) => { )} {(!siteinfo.currencyISOCode || !siteinfo.paymentMethod) && (

-

Start earning

+

Start earning 💸

Update your payment details to sell paid products.

diff --git a/apps/web/components/admin/page-editor/index.tsx b/apps/web/components/admin/page-editor/index.tsx index 519210446..25b213762 100644 --- a/apps/web/components/admin/page-editor/index.tsx +++ b/apps/web/components/admin/page-editor/index.tsx @@ -352,7 +352,7 @@ function PageEditor({ layout?: Record[]; title?: string; description?: string; - socialImage?: Media | {}; + socialImage?: Media | null; robotsAllowed?: boolean; }) => { if (!pageId) { @@ -573,9 +573,7 @@ function PageEditor({ ? page.robotsAllowed : true } - socialImage={ - page.draftSocialImage || page.socialImage || {} - } + socialImage={page.draftSocialImage || {}} onClose={(e) => setLeftPaneContent("none")} onSave={({ title, diff --git a/apps/web/components/admin/page-editor/seo-editor.tsx b/apps/web/components/admin/page-editor/seo-editor.tsx index ca26b5608..741fcbd28 100644 --- a/apps/web/components/admin/page-editor/seo-editor.tsx +++ b/apps/web/components/admin/page-editor/seo-editor.tsx @@ -90,32 +90,6 @@ function SeoEditor({ setInnerDescription(e.target.value) } /> - - { - if (media) { - setInnerSocialImage(media); - } - }} - onRemove={() => { - setInnerSocialImage({}); - }} - strings={{}} - access="public" - mediaId={innerSocialImage && innerSocialImage.mediaId} - />
+
+ { + if (media) { + setInnerSocialImage(media); + onSave({ socialImage: media }); + } + }} + onRemove={() => { + setInnerSocialImage({}); + onSave({ socialImage: null }); + }} + strings={{}} + access="public" + mediaId={innerSocialImage && innerSocialImage.mediaId} + type="page" + /> +
); } diff --git a/apps/web/components/admin/products/editor/content/lesson.tsx b/apps/web/components/admin/products/editor/content/lesson.tsx index 235cf2889..25d55ddb2 100644 --- a/apps/web/components/admin/products/editor/content/lesson.tsx +++ b/apps/web/components/admin/products/editor/content/lesson.tsx @@ -9,7 +9,6 @@ import { CONTENT_URL_LABEL, LESSON_PREVIEW, DELETE_LESSON_POPUP_HEADER, - POPUP_CANCEL_ACTION, APP_MESSAGE_LESSON_DELETED, BUTTON_NEW_LESSON_TEXT, EDIT_LESSON_TEXT, @@ -18,6 +17,8 @@ import { LESSON_CONTENT_EMBED_HEADER, LESSON_CONTENT_EMBED_PLACEHOLDER, BUTTON_SAVING, + MANAGE_COURSES_PAGE_HEADING, + BREADCRUMBS_EDIT_LESSON_COURSE_NAME, } from "@ui-config/strings"; import { LESSON_TYPE_TEXT, @@ -46,7 +47,7 @@ import type { import { actionCreators } from "@courselit/state-management"; import { useRouter } from "next/router"; import useCourse from "../course-hook"; -import { Help } from "@courselit/icons"; +import { Help, Info } from "@courselit/icons"; import { Tooltip, Link, @@ -60,6 +61,7 @@ import { FormField, Switch, TextEditorEmptyDoc, + Breadcrumbs, } from "@courselit/components-library"; import { QuizBuilder } from "./quiz-builder"; @@ -198,26 +200,6 @@ const LessonEditor = ({ title: "${lesson.title}", downloadable: ${lesson.downloadable}, content: ${formatContentForSending()}, - media: ${ - lesson.media && lesson.media.mediaId - ? `{ - mediaId: "${lesson.media.mediaId}", - originalFileName: "${ - lesson.media.originalFileName - }", - mimeType: "${lesson.media.mimeType}", - size: ${lesson.media.size || 0}, - access: "${lesson.media.access}", - file: ${ - lesson.media.access === "public" - ? `"${lesson.media.file}"` - : null - }, - thumbnail: "${lesson.media.thumbnail}", - caption: "${lesson.media.caption}" - }` - : null - }, requiresEnrollment: ${lesson.requiresEnrollment} }) { lessonId @@ -234,7 +216,46 @@ const LessonEditor = ({ dispatch(networkAction(true)); setLoading(true); await fetch.exec(); - goBackLessonList(); + } catch (err: any) { + dispatch(setAppMessage(new AppMessage(err.message))); + } finally { + dispatch(networkAction(false)); + setLoading(false); + } + }; + + const saveMediaContent = async (media?: Media) => { + const query = ` + mutation ($id: ID!, $media: MediaInput) { + lesson: updateLesson(lessonData: { + id: $id + media: $media + }) { + lessonId + } + } + `; + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query, + variables: { + id: lesson.lessonId, + media: media + ? Object.assign({}, media, { + file: + media.access === "public" ? media.file : null, + }) + : null, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + + try { + dispatch(networkAction(true)); + setLoading(true); + await fetch.exec(); } catch (err: any) { dispatch(setAppMessage(new AppMessage(err.message))); } finally { @@ -262,24 +283,6 @@ const LessonEditor = ({ downloadable: ${lesson.downloadable}, type: ${lesson.type.toUpperCase()}, content: ${formatContentForSending()}, - media: ${ - lesson.media && lesson.media.mediaId - ? `{ - mediaId: "${lesson.media.mediaId}", - originalFileName: "${lesson.media.originalFileName}", - mimeType: "${lesson.media.mimeType}", - size: ${lesson.media.size || 0}, - access: "${lesson.media.access}", - file: ${ - lesson.media.access === "public" - ? `"${lesson.media.file}"` - : null - }, - thumbnail: "${lesson.media.thumbnail}", - caption: "${lesson.media.caption}" - }` - : null - }, courseId: "${lesson.courseId}", requiresEnrollment: ${lesson.requiresEnrollment}, groupId: "${lesson.groupId}" @@ -297,8 +300,14 @@ const LessonEditor = ({ try { dispatch(networkAction(true)); setLoading(true); - await fetch.exec(); - goBackLessonList(); + const response = await fetch.exec(); + if (response.lesson) { + setLesson( + Object.assign({}, lesson, { + lessonId: response.lesson.lessonId, + }), + ); + } } catch (err: any) { dispatch(setAppMessage(new AppMessage(err.message))); } finally { @@ -371,9 +380,6 @@ const LessonEditor = ({ return [...MIMETYPE_AUDIO, ...MIMETYPE_VIDEO, ...MIMETYPE_PDF]; }; - const goBackLessonList = () => - router.replace(`/dashboard/product/${courseId}/content`); - const selectOptions = course?.type === COURSE_TYPE_COURSE.toUpperCase() ? [ @@ -452,179 +458,212 @@ const LessonEditor = ({ } return ( -
-
-

- {lessonId ? EDIT_LESSON_TEXT : BUTTON_NEW_LESSON_TEXT} -

-
- {course?.type?.toLowerCase() === COURSE_TYPE_COURSE && ( - - )} - {course?.type?.toLowerCase() === COURSE_TYPE_COURSE && ( - { setLesson( Object.assign({}, lesson, { - downloadable: value, + type: value, }), ); }} + disabled={!!lesson.lessonId} /> -
- )} - {lesson.type.toLowerCase() !== LESSON_TYPE_QUIZ && ( -
-
-

{LESSON_PREVIEW}

- - - + )} + {lesson.type.toLowerCase() === LESSON_TYPE_TEXT && ( +
+

{LESSON_CONTENT_HEADER}

+ + setTextContent(state) + } + url={address.backend} + />
- { - setLesson( - Object.assign({}, lesson, { - requiresEnrollment: !value, - }), - ); - }} + )} + {lesson.type.toLowerCase() === LESSON_TYPE_QUIZ && ( + setQuizContent(state)} /> -
- )} -
-
- - {courseId && ( - + , + ) => + setContent({ + value: e.target.value, + }) + } + /> +
+ )} + {[ + LESSON_TYPE_VIDEO, + LESSON_TYPE_AUDIO, + LESSON_TYPE_PDF, + ].includes(lesson.type) && ( +
+

{DOWNLOADABLE_SWITCH}

+ { + setLesson( + Object.assign({}, lesson, { + downloadable: value, + }), + ); + }} + /> +
+ )} + {lesson.type.toLowerCase() !== LESSON_TYPE_QUIZ && ( +
+
+

{LESSON_PREVIEW}

+ + + +
+ { + setLesson( + Object.assign({}, lesson, { + requiresEnrollment: !value, + }), + ); + }} + /> +
+ )} +
+
+ + {/* {courseId && ( + + + + )} */} +
+ + {BUTTON_DELETE_LESSON_TEXT} - - )} + } + onClick={onLessonDelete} + >
- {BUTTON_DELETE_LESSON_TEXT} + +
+
+ {![ + String.prototype.toUpperCase.call(LESSON_TYPE_TEXT), + String.prototype.toUpperCase.call(LESSON_TYPE_QUIZ), + String.prototype.toUpperCase.call(LESSON_TYPE_EMBED), + ].includes(lesson.type) && ( +
+ { + if (media) { + setLesson( + Object.assign({}, lesson, { + title: + lesson.title || + media.originalFileName, + media, + }), + ); + saveMediaContent(media); } - onClick={onLessonDelete} - > -
- - - + }} + mimeTypesToShow={getMimeTypesToShow()} + strings={{}} + auth={auth} + profile={profile} + dispatch={dispatch} + address={address} + mediaId={lesson.media?.mediaId} + onRemove={() => { + setLesson( + Object.assign({}, lesson, { + media: {}, + }), + ); + saveMediaContent(); + }} + type="lesson" + /> + {!(lesson.lessonId && lesson.title) && ( +

+ + Set the title of the lesson to enable media upload +

+ )} + + )} + ); }; diff --git a/apps/web/components/admin/products/editor/details.tsx b/apps/web/components/admin/products/editor/details.tsx index 4dd10f3ac..c66b9c08e 100644 --- a/apps/web/components/admin/products/editor/details.tsx +++ b/apps/web/components/admin/products/editor/details.tsx @@ -7,6 +7,7 @@ import { Button, Form, FormField, + PageBuilderPropertyHeader, } from "@courselit/components-library"; import useCourse from "./course-hook"; import { FetchBuilder } from "@courselit/utils"; @@ -24,6 +25,7 @@ import { import { APP_MESSAGE_COURSE_SAVED, BUTTON_SAVE, + COURSE_CONTENT_HEADER, FORM_FIELD_FEATURED_IMAGE, } from "../../../../ui-config/strings"; import { connect } from "react-redux"; @@ -66,25 +68,7 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { updateCourse(courseData: { id: "${course!.id}", title: "${title}", - description: ${JSON.stringify(JSON.stringify(description))}, - featuredImage: ${ - featuredImage.mediaId - ? `{ - mediaId: "${featuredImage.mediaId}", - originalFileName: "${featuredImage.originalFileName}", - mimeType: "${featuredImage.mimeType}", - size: ${featuredImage.size}, - access: "${featuredImage.access}", - file: ${ - featuredImage.access === "public" - ? `"${featuredImage.file}"` - : null - }, - thumbnail: "${featuredImage.thumbnail}", - caption: "${featuredImage.caption}" - }` - : null - } + description: ${JSON.stringify(JSON.stringify(description))} }) { id } @@ -110,24 +94,69 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { } }; + const saveFeaturedImage = async (media?: Media) => { + const mutation = ` + mutation ($courseId: ID!, $media: MediaInput) { + updateCourse(courseData: { + id: $courseId + featuredImage: $media + }) { + id + } + } + `; + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query: mutation, + variables: { + courseId: course?.id, + media: media || null, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + try { + dispatch(networkAction(true)); + const response = await fetch.exec(); + if (response.updateCourse) { + dispatch( + setAppMessage(new AppMessage(APP_MESSAGE_COURSE_SAVED)), + ); + } + } catch (err: any) { + dispatch(setAppMessage(new AppMessage(err.message))); + } finally { + dispatch(networkAction(false)); + } + }; + return ( -
-
- setTitle(e.target.value)} - /> - ) => { - setDescription(state); - }} - refresh={refresh} - url={address.backend} - /> +
+
+ + setTitle(e.target.value)} + /> + + ) => { + setDescription(state); + }} + refresh={refresh} + url={address.backend} + /> +
+ +
+ +
+
{ - media && setFeaturedImage(media || null); + media && setFeaturedImage(media); + saveFeaturedImage(media); }} mimeTypesToShow={[...MIMETYPE_IMAGE]} access="public" @@ -144,16 +174,15 @@ function Details({ id, address, dispatch, auth, profile }: DetailsProps) { profile={profile} dispatch={dispatch} address={address} - mediaId={featuredImage?.mediaId} + mediaId={(featuredImage && featuredImage.mediaId) || ""} onRemove={() => { setFeaturedImage({}); + saveFeaturedImage(); }} + type="course" /> -
- -
- -
+
+ ); } diff --git a/apps/web/components/admin/settings/index.tsx b/apps/web/components/admin/settings/index.tsx index 3bdb53e35..f0152c9c5 100644 --- a/apps/web/components/admin/settings/index.tsx +++ b/apps/web/components/admin/settings/index.tsx @@ -36,11 +36,13 @@ import { SITE_MAILING_ADDRESS_SETTING_HEADER, SITE_MAILING_ADDRESS_SETTING_EXPLANATION, SITE_SETTINGS_RAZORPAY_KEY_TEXT, + MEDIA_SELECTOR_UPLOAD_BTN_CAPTION, + MEDIA_SELECTOR_REMOVE_BTN_CAPTION, } from "../../../ui-config/strings"; import { FetchBuilder, capitalize } from "@courselit/utils"; import { decode, encode } from "base-64"; import { AppMessage, Profile, UIConstants } from "@courselit/common-models"; -import type { SiteInfo, Address, Auth } from "@courselit/common-models"; +import type { SiteInfo, Address, Auth, Media } from "@courselit/common-models"; import type { AppDispatch, AppState } from "@courselit/state-management"; import { actionCreators } from "@courselit/state-management"; import currencies from "@/data/currencies.json"; @@ -213,27 +215,7 @@ const Settings = (props: SettingsProps) => { mutation { settings: updateSiteInfo(siteData: { title: "${newSettings.title}", - subtitle: "${newSettings.subtitle}", - logo: ${ - newSettings.logo && newSettings.logo.mediaId - ? `{ - mediaId: "${newSettings.logo.mediaId}", - originalFileName: "${ - newSettings.logo.originalFileName - }", - mimeType: "${newSettings.logo.mimeType}", - size: ${newSettings.logo.size}, - access: "${newSettings.logo.access}", - file: ${ - newSettings.logo.access === "public" - ? `"${newSettings.logo.file}"` - : null - }, - thumbnail: "${newSettings.logo.thumbnail}", - caption: "${newSettings.logo.caption}" - }` - : null - } + subtitle: "${newSettings.subtitle || ""}", }) { settings { title, @@ -276,6 +258,60 @@ const Settings = (props: SettingsProps) => { } }; + const saveLogo = async (media?: Media) => { + const query = ` + mutation ($logo: MediaInput) { + settings: updateSiteInfo(siteData: { + logo: $logo + }) { + settings { + title, + subtitle, + logo { + mediaId, + originalFileName, + mimeType, + size, + access, + file, + thumbnail, + caption + }, + currencyISOCode, + paymentMethod, + stripeKey, + razorpayKey, + codeInjectionHead, + codeInjectionBody, + mailingAddress + } + } + }`; + + try { + const fetchRequest = fetch + .setPayload({ + query, + variables: { + logo: media || null, + }, + }) + .build(); + props.dispatch(networkAction(true)); + const response = await fetchRequest.exec(); + if (response.settings.settings) { + setSettingsState(response.settings.settings); + props.dispatch( + setAppMessage(new AppMessage(APP_MESSAGE_SETTINGS_SAVED)), + ); + } + } catch (e: any) { + props.dispatch(setAppMessage(new AppMessage(e.message))); + } finally { + props.dispatch(networkAction(false)); + } + }; + const handleCodeInjectionSettingsSubmit = async ( event: React.FormEvent, ) => { @@ -545,70 +581,77 @@ const Settings = (props: SettingsProps) => { ]} defaultValue={selectedTab} > -
- - - - - { - setNewSettings( - Object.assign({}, newSettings, { - logo: {}, - }), - ); - }} - /> -
- + title: settings.title, + subtitle: settings.subtitle, + logo: settings.logo, + }) === + JSON.stringify({ + title: newSettings.title, + subtitle: newSettings.subtitle, + logo: newSettings.logo, + }) || + !newSettings.title || + props.networkAction + } + > + {BUTTON_SAVE} + +
+ +
+ + { + if (media) { + saveLogo(media); + } + }} + mimeTypesToShow={[...MIMETYPE_IMAGE]} + access="public" + strings={{ + buttonCaption: + MEDIA_SELECTOR_UPLOAD_BTN_CAPTION, + removeButtonCaption: + MEDIA_SELECTOR_REMOVE_BTN_CAPTION, + }} + mediaId={newSettings.logo?.mediaId} + onRemove={() => saveLogo()} + type="domain" + />
- +
{ loading={loading} > {users.map((user) => ( - +
@@ -287,7 +280,7 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => { src={ user.avatar ? user.avatar?.file - : "/favicon.ico" + : "/courselit_backdrop_square.webp" } /> diff --git a/apps/web/components/admin/users/permissions-to-caption-map.ts b/apps/web/components/admin/users/permissions-to-caption-map.ts index 5f50fb375..d52028ffc 100644 --- a/apps/web/components/admin/users/permissions-to-caption-map.ts +++ b/apps/web/components/admin/users/permissions-to-caption-map.ts @@ -6,9 +6,6 @@ import { PERM_COURSE_PUBLISH, PERM_ENROLL_IN_COURSE, PERM_MEDIA_MANAGE, - PERM_MEDIA_MANAGE_ANY, - PERM_MEDIA_VIEW_ANY, - PERM_MEDIA_UPLOAD, PERM_SETTINGS, PERM_USERS, PERM_SITE, @@ -19,10 +16,7 @@ const permissionToCaptionMap = { [permissions.manageAnyCourse]: PERM_COURSE_MANAGE_ANY, [permissions.publishCourse]: PERM_COURSE_PUBLISH, [permissions.enrollInCourse]: PERM_ENROLL_IN_COURSE, - [permissions.viewAnyMedia]: PERM_MEDIA_VIEW_ANY, - [permissions.uploadMedia]: PERM_MEDIA_UPLOAD, [permissions.manageMedia]: PERM_MEDIA_MANAGE, - [permissions.manageAnyMedia]: PERM_MEDIA_MANAGE_ANY, [permissions.manageSite]: PERM_SITE, [permissions.manageSettings]: PERM_SETTINGS, [permissions.manageUsers]: PERM_USERS, diff --git a/apps/web/components/public/article.tsx b/apps/web/components/public/article.tsx index 7146f7d71..9e93e7331 100644 --- a/apps/web/components/public/article.tsx +++ b/apps/web/components/public/article.tsx @@ -46,7 +46,7 @@ const Article = (props: ArticleProps) => { src={ profile.avatar ? profile.avatar?.file - : "/favicon.ico" + : "/courselit_backdrop_square.webp" } /> diff --git a/apps/web/components/public/base-layout/branding.tsx b/apps/web/components/public/base-layout/branding.tsx index 4acd2a2c0..8642be10d 100644 --- a/apps/web/components/public/base-layout/branding.tsx +++ b/apps/web/components/public/base-layout/branding.tsx @@ -15,9 +15,9 @@ const Branding = ({ siteinfo }: BrandingProps) => {
logo
diff --git a/apps/web/components/public/base-layout/index.tsx b/apps/web/components/public/base-layout/index.tsx index 70e377ff3..7c7506d4a 100644 --- a/apps/web/components/public/base-layout/index.tsx +++ b/apps/web/components/public/base-layout/index.tsx @@ -27,7 +27,7 @@ interface MasterLayoutProps { theme: Theme; dispatch: AppDispatch; description?: string; - socialImage: Media; + socialImage?: Media; robotsAllowed?: boolean; } diff --git a/apps/web/config/strings.ts b/apps/web/config/strings.ts index d8c321976..545a2aec0 100644 --- a/apps/web/config/strings.ts +++ b/apps/web/config/strings.ts @@ -20,7 +20,7 @@ export const responses = { user_not_found: "User not found.", request_not_authenticated: "Request not authenticated", content_cannot_be_null: "Content cannot be empty", - media_id_cannot_be_null: "Media Id cannot be empty", + media_id_cannot_be_null: "Media cannot be empty", item_not_found: "Item not found", drip_not_released: "This section is not yet released for you", not_a_creator: "You do not have rights to perform this action", diff --git a/apps/web/graphql/lessons/helpers.ts b/apps/web/graphql/lessons/helpers.ts index a114771e5..7717fb40a 100644 --- a/apps/web/graphql/lessons/helpers.ts +++ b/apps/web/graphql/lessons/helpers.ts @@ -14,10 +14,10 @@ type LessonValidatorProps = Pick< export const lessonValidator = (lessonData: LessonValidatorProps) => { validateTextContent(lessonData); - validateMediaContent(lessonData); + // validateMediaContent(lessonData); }; -function validateTextContent(lessonData: LessonValidatorProps) { +export function validateTextContent(lessonData: LessonValidatorProps) { const content = lessonData.content ? JSON.parse(lessonData.content) : null; if ([text, embed].includes(lessonData.type)) { diff --git a/apps/web/graphql/lessons/types.ts b/apps/web/graphql/lessons/types.ts index a73593077..342885816 100644 --- a/apps/web/graphql/lessons/types.ts +++ b/apps/web/graphql/lessons/types.ts @@ -93,7 +93,7 @@ const lessonInputType = new GraphQLInputObjectType({ type: new GraphQLNonNull(GraphQLBoolean), }, content: { type: GraphQLString }, - media: { type: mediaTypes.mediaInputType }, + // media: { type: mediaTypes.mediaInputType }, downloadable: { type: GraphQLBoolean }, groupId: { type: new GraphQLNonNull(GraphQLID) }, }, diff --git a/apps/web/graphql/pages/logic.ts b/apps/web/graphql/pages/logic.ts index db3a49bec..100dce86d 100644 --- a/apps/web/graphql/pages/logic.ts +++ b/apps/web/graphql/pages/logic.ts @@ -237,7 +237,7 @@ export const updatePage = async ({ if (description) { page.draftDescription = description; } - if (socialImage) { + if (typeof socialImage !== "undefined") { page.draftSocialImage = socialImage; } if (typeof robotsAllowed === "boolean") { @@ -286,14 +286,12 @@ export const publish = async ( page.description = page.draftDescription; page.draftDescription = undefined; } - if (page.draftSocialImage) { - page.socialImage = page.draftSocialImage; - page.draftSocialImage = undefined; - } if (page.draftRobotsAllowed) { page.robotsAllowed = page.draftRobotsAllowed; page.draftRobotsAllowed = undefined; } + page.socialImage = page.draftSocialImage; + ctx.subdomain.typefaces = ctx.subdomain.draftTypefaces; ctx.subdomain.sharedWidgets = ctx.subdomain.draftSharedWidgets; ctx.subdomain.draftSharedWidgets = {}; diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts index e7543c221..b30389a53 100644 --- a/apps/web/graphql/users/logic.ts +++ b/apps/web/graphql/users/logic.ts @@ -105,7 +105,7 @@ export const updateUser = async (userData: any, ctx: GQLContext) => { permissions.manageUsers, ]); if (!hasPermissionToManageUser) { - if (id !== ctx.user._id) { + if (id !== ctx.user._id.toString()) { throw new Error(responses.action_not_allowed); } } @@ -118,9 +118,12 @@ export const updateUser = async (userData: any, ctx: GQLContext) => { continue; } - if (!["subscribedToUpdates"].includes(key) && id === ctx.user._id) { - throw new Error(responses.action_not_allowed); - } + // if ( + // !["subscribedToUpdates"].includes(key) && + // id === ctx.user._id.toString() + // ) { + // throw new Error(responses.action_not_allowed); + // } if (key === "tags") { addTags(userData["tags"], ctx); @@ -282,17 +285,16 @@ export async function createUser({ constants.permissions.manageCourse, constants.permissions.manageAnyCourse, constants.permissions.publishCourse, - // TODO: replace media perms with course perms constants.permissions.manageMedia, - constants.permissions.manageAnyMedia, - constants.permissions.uploadMedia, - constants.permissions.viewAnyMedia, constants.permissions.manageSite, constants.permissions.manageSettings, constants.permissions.manageUsers, ]; } else { - newUser.permissions = [constants.permissions.enrollInCourse]; + newUser.permissions = [ + constants.permissions.enrollInCourse, + constants.permissions.manageMedia, + ]; } newUser.lead = lead; const user = await UserModel.create(newUser); diff --git a/apps/web/models/Media.ts b/apps/web/models/Media.ts index 3efdc98a1..c051072fb 100644 --- a/apps/web/models/Media.ts +++ b/apps/web/models/Media.ts @@ -3,7 +3,9 @@ import mongoose from "mongoose"; import constants from "../config/constants"; const { publicMedia, privateMedia } = constants; -const MediaSchema = new mongoose.Schema({ +type MediaWithOwner = Media & { userId: string }; + +const MediaSchema = new mongoose.Schema({ mediaId: { type: String, required: true }, originalFileName: { type: String, required: true }, mimeType: { type: String, required: true }, diff --git a/apps/web/pages/api/media/[mediaId].ts b/apps/web/pages/api/media/[mediaId].ts deleted file mode 100644 index 7908ae0af..000000000 --- a/apps/web/pages/api/media/[mediaId].ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { responses } from "../../../config/strings"; -import * as medialitService from "../../../services/medialit"; -import { UIConstants as constants } from "@courselit/common-models"; -import { checkPermission } from "@courselit/utils"; -import User from "@models/User"; -import DomainModel, { Domain } from "@models/Domain"; -import { auth } from "@/auth"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse, -) { - if (req.method !== "DELETE") { - return res.status(405).json({ message: "Not allowed" }); - } - - const domain = await DomainModel.findOne({ - name: req.headers.domain, - }); - if (!domain) { - return res.status(404).json({ message: "Domain not found" }); - } - - const session = await auth(req, res); - - let user; - if (session) { - user = await User.findOne({ - email: session.user!.email, - domain: domain._id, - active: true, - }); - } - - if (!user) { - return res.status(401).json({}); - } - - if ( - !checkPermission(user!.permissions, [ - constants.permissions.manageAnyCourse, - constants.permissions.manageCourse, - ]) - ) { - throw new Error(responses.action_not_allowed); - } - - const { mediaId } = req.query; - try { - let response = await medialitService.deleteMedia(mediaId); - return res.status(200).json({ message: responses.success }); - } catch (err: any) { - return res.status(500).json({ error: responses.internal_error }); - } -} diff --git a/apps/web/pages/api/media/[mediaId]/[type].ts b/apps/web/pages/api/media/[mediaId]/[type].ts new file mode 100644 index 000000000..8bcdae174 --- /dev/null +++ b/apps/web/pages/api/media/[mediaId]/[type].ts @@ -0,0 +1,160 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { responses } from "@/config/strings"; +import * as medialitService from "@/services/medialit"; +import { UIConstants as constants } from "@courselit/common-models"; +import { checkPermission } from "@courselit/utils"; +import UserModel, { User } from "@models/User"; +import DomainModel, { Domain } from "@models/Domain"; +import { auth } from "@/auth"; +import CourseModel, { Course } from "@models/Course"; +import LessonModel, { Lesson } from "@models/Lesson"; +import PageModel, { Page } from "@models/Page"; + +const types = ["course", "lesson", "page", "user", "domain"] as const; + +type MediaType = (typeof types)[number]; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "DELETE") { + return res.status(405).json({ message: "Not allowed" }); + } + + const domain = await DomainModel.findOne({ + name: req.headers.domain, + }); + if (!domain) { + return res.status(404).json({ message: "Domain not found" }); + } + + const session = await auth(req, res); + + let user; + if (session) { + user = await UserModel.findOne({ + email: session.user!.email, + domain: domain._id, + active: true, + }); + } + + if (!user) { + return res.status(401).json({}); + } + + const { mediaId, type } = req.query; + if (!types.includes(type as MediaType)) { + return res.status(400).json({ message: "Bad request" }); + } + + if ( + !(await isActionAllowed(user, type as any, mediaId as string, domain)) + ) { + (""); + return res.status(403).json({ message: responses.action_not_allowed }); + } + + try { + let response = await medialitService.deleteMedia(mediaId); + return res.status(200).json({ message: responses.success }); + } catch (err: any) { + return res.status(500).json({ error: responses.internal_error }); + } +} + +async function isActionAllowed( + user: User, + type: MediaType, + mediaId: string, + domain: Domain, +) { + if ( + !checkPermission(user.permissions, [constants.permissions.manageMedia]) + ) { + return false; + } + + switch (type) { + case "course": + const course = await CourseModel.findOne({ + domain: domain._id, + "featuredImage.mediaId": mediaId, + }); + if (!course) { + return false; + } + if ( + checkPermission(user.permissions, [ + constants.permissions.manageAnyCourse, + ]) + ) { + return true; + } else { + return ( + course.creatorId === user.userId && + checkPermission(user.permissions, [ + constants.permissions.manageCourse, + ]) + ); + } + case "lesson": + const lesson = await LessonModel.findOne({ + domain: domain._id, + "media.mediaId": mediaId, + }); + if (!lesson) { + return false; + } + if ( + checkPermission(user.permissions, [ + constants.permissions.manageAnyCourse, + ]) + ) { + return true; + } else { + return ( + lesson?.creatorId.toString() === user._id.toString() && + checkPermission(user.permissions, [ + constants.permissions.manageCourse, + ]) + ); + } + case "page": + const pages = await PageModel.find({ + domain: domain._id, + }); + let mediaBelongsToThisDomain = false; + for (const p of pages) { + const fullContent = + JSON.stringify(p.layout) + + JSON.stringify(p.draftLayout) + + JSON.stringify(p.socialImage) + + JSON.stringify(p.draftSocialImage); + if (fullContent.indexOf(`"mediaId":"${mediaId}"`) !== -1) { + mediaBelongsToThisDomain = true; + break; + } + } + return ( + mediaBelongsToThisDomain && + checkPermission(user.permissions, [ + constants.permissions.manageSite, + ]) + ); + case "user": + return ( + user.avatar.mediaId === mediaId || + checkPermission(user.permissions, [ + constants.permissions.manageUsers, + ]) + ); + case "domain": + return checkPermission(user.permissions, [ + constants.permissions.manageSettings, + ]); + default: + return false; + } +} diff --git a/apps/web/pages/api/media/presigned.ts b/apps/web/pages/api/media/presigned.ts index 6fe1c43c9..ee260a2fd 100644 --- a/apps/web/pages/api/media/presigned.ts +++ b/apps/web/pages/api/media/presigned.ts @@ -6,6 +6,7 @@ import { checkPermission } from "@courselit/utils"; import User from "@models/User"; import DomainModel, { Domain } from "@models/Domain"; import { auth } from "@/auth"; +import { error } from "@/services/logger"; export default async function handler( req: NextApiRequest, @@ -38,16 +39,20 @@ export default async function handler( } if ( - !checkPermission(user!.permissions, [constants.permissions.uploadMedia]) + !checkPermission(user!.permissions, [constants.permissions.manageMedia]) ) { - throw new Error(responses.action_not_allowed); + return res.status(403).json({ message: responses.action_not_allowed }); } + try { let response = await medialitService.getPresignedUrlForUpload( domain.name, ); return res.status(200).json({ url: response }); } catch (err: any) { + error(err.mssage, { + stack: err.stack, + }); return res.status(500).json({ error: err.message }); } } diff --git a/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx b/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx index 8ca6ebf8d..938efe0db 100644 --- a/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx +++ b/apps/web/pages/dashboard/product/[id]/section/[section]/lesson/new.tsx @@ -1,15 +1,10 @@ import dynamic from "next/dynamic"; import { useRouter } from "next/router"; -import { BUTTON_NEW_LESSON_TEXT } from "../../../../../../../ui-config/strings"; +import { BUTTON_NEW_LESSON_TEXT } from "@/ui-config/strings"; -const BaseLayout = dynamic( - () => import("../../../../../../../components/admin/base-layout"), -); +const BaseLayout = dynamic(() => import("@/components/admin/base-layout")); const LessonEditor = dynamic( - () => - import( - "../../../../../../../components/admin/products/editor/content/lesson" - ), + () => import("@/components/admin/products/editor/content/lesson"), ); function NewLesson({}) { diff --git a/apps/web/pages/login.tsx b/apps/web/pages/login.tsx index 5df3e2f4c..9a7c35566 100644 --- a/apps/web/pages/login.tsx +++ b/apps/web/pages/login.tsx @@ -147,7 +147,8 @@ const Login = ({ page, auth, dispatch }: LoginProps) => { {showCode && (

- {BTN_LOGIN_CODE_INTIMATION} {email} + {BTN_LOGIN_CODE_INTIMATION}{" "} + {email}

{ + const mutation = ` + mutation ($id: ID!, $avatar: MediaInput) { + user: updateUser(userData: { + id: $id + avatar: $avatar + }) { + id, + name, + bio, + avatar { + mediaId, + originalFileName, + mimeType, + size, + access, + file, + thumbnail, + caption + }, + } + } + `; + const fetch = new FetchBuilder() + .setUrl(`${address.backend}/api/graph`) + .setPayload({ + query: mutation, + variables: { + id: profile.id, + avatar: media || null, + }, + }) + .setIsGraphQLEndpoint(true) + .build(); + + try { + dispatch(networkAction(true)); + await fetch.exec(); + dispatch(refreshUserProfile()); + dispatch(setAppMessage(new AppMessage(APP_MESSAGE_CHANGES_SAVED))); + } catch (err: any) { + dispatch(setAppMessage(new AppMessage(err.message))); + } finally { + dispatch(networkAction(false)); + } + }; + const saveDetails = async (e: FormEvent) => { e.preventDefault(); const mutation = ` - mutation { + mutation ($id: ID!, $name: String, $bio: String) { user: updateUser(userData: { - id: "${profile.id}" - name: "${name}", - bio: "${bio}" - avatar: ${ - avatar && avatar.mediaId - ? `{ - mediaId: "${avatar.mediaId}", - originalFileName: "${avatar.originalFileName}", - mimeType: "${avatar.mimeType}", - size: ${avatar.size}, - access: "${avatar.access}", - file: ${ - avatar.access === "public" - ? `"${avatar.file}"` - : null - }, - thumbnail: "${avatar.thumbnail}", - caption: "${avatar.caption}" - }` - : null - } + id: $id + name: $name + bio: $bio }) { id, name, @@ -162,7 +198,14 @@ function ProfileIndex({ `; const fetch = new FetchBuilder() .setUrl(`${address.backend}/api/graph`) - .setPayload(mutation) + .setPayload({ + query: mutation, + variables: { + id: profile.id, + name, + bio, + }, + }) .setIsGraphQLEndpoint(true) .build(); @@ -181,10 +224,10 @@ function ProfileIndex({ const saveEmailPreference = async function (state: boolean) { setSubscribedToUpdates(state); const mutation = ` - mutation { + mutation ($id: ID!, $subscribedToUpdates: Boolean) { user: updateUser(userData: { - id: "${profile.id}" - subscribedToUpdates: ${state} + id: $id + subscribedToUpdates: $subscribedToUpdates }) { subscribedToUpdates } @@ -192,7 +235,13 @@ function ProfileIndex({ `; const fetch = new FetchBuilder() .setUrl(`${address.backend}/api/graph`) - .setPayload(mutation) + .setPayload({ + query: mutation, + variables: { + id: profile.id, + subscribedToUpdates: state, + }, + }) .setIsGraphQLEndpoint(true) .build(); @@ -214,33 +263,23 @@ function ProfileIndex({

{PROFILE_PAGE_HEADER}

- -
- {}} - disabled={true} - /> - - - setName(event.target.value) - } - /> - setBio(event.target.value)} - label={PROFILE_SECTION_DETAILS_BIO} - multiline={true} - maxRows={5} +
+
+ - - + + + + profile pic + + { if (media) { - setAvatar(media); + updateProfilePic(media); } }} onRemove={() => { - setAvatar({}); + updateProfilePic(); }} access="public" - strings={{}} + strings={{ + buttonCaption: + MEDIA_SELECTOR_UPLOAD_BTN_CAPTION, + removeButtonCaption: + MEDIA_SELECTOR_REMOVE_BTN_CAPTION, + }} + type="user" + hidePreview={true} /> - -
- - {BUTTON_SAVE} - -
- +
+
+ {}} + disabled={true} + /> + + setName(event.target.value) + } + /> + + setBio(event.target.value) + } + label={PROFILE_SECTION_DETAILS_BIO} + multiline={true} + maxRows={5} + /> +
+ + {BUTTON_SAVE} + +
+
+
+

diff --git a/apps/web/services/logger.ts b/apps/web/services/logger.ts index 35ff9a7e2..b15b23520 100644 --- a/apps/web/services/logger.ts +++ b/apps/web/services/logger.ts @@ -7,22 +7,30 @@ export const info = async ( message: string, metadata?: Record, ) => { - await Log.create({ - severity: severityInfo, - message, - metadata, - }); + if (process.env.NODE_ENV === "production") { + await Log.create({ + severity: severityInfo, + message, + metadata, + }); + } else { + console.error(severityError, message, metadata); + } }; export const warn = async ( message: string, metadata?: Record, ) => { - await Log.create({ - severity: severityWarn, - message, - metadata, - }); + if (process.env.NODE_ENV === "production") { + await Log.create({ + severity: severityWarn, + message, + metadata, + }); + } else { + console.error(severityError, message, metadata); + } }; export const error = async ( @@ -33,9 +41,13 @@ export const error = async ( [x: string]: any; }, ) => { - await Log.create({ - severity: severityError, - message, - metadata, - }); + if (process.env.NODE_ENV === "production") { + await Log.create({ + severity: severityError, + message, + metadata, + }); + } else { + console.error(severityError, message, metadata); + } }; diff --git a/apps/web/ui-config/strings.ts b/apps/web/ui-config/strings.ts index e403dbd05..b5231b936 100644 --- a/apps/web/ui-config/strings.ts +++ b/apps/web/ui-config/strings.ts @@ -39,6 +39,7 @@ export const LOAD_MORE_TEXT = "Load more"; export const MANAGE_MEDIA_BUTTON_TEXT = "Insert media"; export const MANAGE_COURSES_PAGE_HEADING = "Products"; export const MANAGE_PAGES_PAGE_HEADING = "Pages"; +export const BREADCRUMBS_EDIT_LESSON_COURSE_NAME = "Product"; export const NEW_PAGE_HEADING = "New page"; export const USERS_MANAGER_PAGE_HEADING = "Users"; export const BTN_MANAGE_TAGS = "Manage tags"; @@ -84,8 +85,10 @@ export const HEADER_TAG_SECTION = "Content tagged with"; export const SITE_SETTINGS_TITLE = "Title"; export const SITE_SETTINGS_SUBTITLE = "Subtitle"; export const SITE_SETTINGS_CURRENCY = "Currency"; -export const SITE_SETTINGS_LOGO = "Brand Logo"; +export const SITE_SETTINGS_LOGO = "Logo"; export const SITE_SETTINGS_PAGE_HEADING = "Settings"; +export const MEDIA_SELECTOR_UPLOAD_BTN_CAPTION = "Upload a picture"; +export const MEDIA_SELECTOR_REMOVE_BTN_CAPTION = "Remove picture"; export const SITE_ADMIN_SETTINGS_STRIPE_SECRET = "Stripe Secret Key"; export const SITE_ADMIN_SETTINGS_RAZORPAY_SECRET = "Razorpay Secret Key"; export const SITE_ADMIN_SETTINGS_RAZORPAY_WEBHOOK_SECRET = @@ -140,8 +143,7 @@ export const LOGIN_FORM_LABEL = "Enter your email to sign in or create an account"; export const LOGIN_NO_CODE = "Did not get the code?"; export const BTN_LOGIN_GET_CODE = "Get code"; -export const BTN_LOGIN_CODE_INTIMATION = - "An email with the code has been sent to"; +export const BTN_LOGIN_CODE_INTIMATION = "Enter the code sent to"; export const LOGIN_FORM_DISCLAIMER = "By submitting, you accept the "; export const SIGNUP_SECTION_HEADER = "Create an account"; export const SIGNUP_SECTION_BUTTON = "Join"; @@ -154,6 +156,7 @@ export const BLOG_POST_SWITCH = "Post"; export const DOWNLOADABLE_SWITCH = "Downloadable"; export const TYPE_DROPDOWN = "Type"; export const LESSON_CONTENT_HEADER = "Text Content"; +export const COURSE_CONTENT_HEADER = "Content"; export const LESSON_CONTENT_EMBED_HEADER = "Link"; export const LESSON_CONTENT_EMBED_PLACEHOLDER = "A link to YouTube video etc."; export const CONTENT_URL_LABEL = "Media content"; @@ -275,6 +278,7 @@ export const PROFILE_SECTION_DETAILS = "Personal details"; export const PROFILE_SECTION_DETAILS_NAME = "Name"; export const PROFILE_SECTION_DETAILS_EMAIL = "Email"; export const PROFILE_SECTION_DETAILS_BIO = "Bio"; +export const PROFILE_SECTION_DISPLAY_PICTURE = "Profile photo"; export const PROFILE_EMAIL_PREFERENCES_NEWSLETTER_OPTION_TEXT = "Receive newsletter and marketing emails"; export const BTN_PUBLISH = "Publish"; @@ -331,10 +335,8 @@ export const PERM_COURSE_MANAGE = "Manage products"; export const PERM_COURSE_MANAGE_ANY = "Manage all products"; export const PERM_COURSE_PUBLISH = "Manage blog"; export const PERM_ENROLL_IN_COURSE = "Buy products"; -export const PERM_MEDIA_UPLOAD = "Upload files"; export const PERM_MEDIA_MANAGE = "Manage files"; export const PERM_MEDIA_MANAGE_ANY = "Manage all files"; -export const PERM_MEDIA_VIEW_ANY = "View files"; export const PERM_SITE = "Manage pages"; export const PERM_SETTINGS = "Manage settings"; export const PERM_USERS = "Manage users"; diff --git a/apps/web/ui-lib/utils.ts b/apps/web/ui-lib/utils.ts index 077f0b408..6a28339d7 100644 --- a/apps/web/ui-lib/utils.ts +++ b/apps/web/ui-lib/utils.ts @@ -50,12 +50,9 @@ export const canAccessDashboard = (profile: Profile) => { return checkPermission(profile.permissions, [ permissions.manageCourse, permissions.manageAnyCourse, - permissions.manageMedia, - permissions.manageAnyMedia, permissions.manageSite, permissions.manageSettings, permissions.manageUsers, - permissions.viewAnyMedia, ]); }; diff --git a/packages/common-models/src/ui-constants.ts b/packages/common-models/src/ui-constants.ts index acb3e36cc..4264a37be 100644 --- a/packages/common-models/src/ui-constants.ts +++ b/packages/common-models/src/ui-constants.ts @@ -46,9 +46,6 @@ export const permissions = { publishCourse: "course:publish", enrollInCourse: "course:enroll", manageMedia: "media:manage", - manageAnyMedia: "media:manage_any", - uploadMedia: "media:upload", - viewAnyMedia: "media:view_any", manageSite: "site:manage", manageSettings: "setting:manage", manageUsers: "user:manage", diff --git a/packages/common-widgets/src/grid/admin-widget/item-editor.tsx b/packages/common-widgets/src/grid/admin-widget/item-editor.tsx index 6a91e1c7c..fe550bd04 100644 --- a/packages/common-widgets/src/grid/admin-widget/item-editor.tsx +++ b/packages/common-widgets/src/grid/admin-widget/item-editor.tsx @@ -107,6 +107,7 @@ export default function ItemEditor({ onRemove={() => { setMedia({}); }} + type="page" /> diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index 5b2377334..14d8f9b81 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -11,13 +11,13 @@ import { } from "@courselit/common-models"; import { AppDispatch } from "@courselit/state-management"; import Access from "./access"; -import Button from "../button"; import Dialog2 from "../dialog2"; import { FetchBuilder } from "@courselit/utils"; import { setAppMessage } from "@courselit/state-management/dist/action-creators"; import Form from "../form"; import FormField from "../form-field"; import React from "react"; +import { Button2, PageBuilderPropertyHeader } from ".."; interface Strings { buttonCaption?: string; @@ -62,6 +62,10 @@ interface MediaSelectorProps { access?: Access; strings: Strings; mediaId?: string; + type: "course" | "lesson" | "page" | "user" | "domain"; + hidePreview?: boolean; + tooltip?: string; + disabled?: boolean; } const MediaSelector = (props: MediaSelectorProps) => { @@ -75,9 +79,18 @@ const MediaSelector = (props: MediaSelectorProps) => { }; const [uploadData, setUploadData] = useState(defaultUploadData); const fileInput: React.RefObject = React.createRef(); - const [selectedFile, setSelectedFile] = useState(); + const [selectedFile, setSelectedFile] = useState(); const [caption, setCaption] = useState(""); - const { strings, dispatch, address, src, title, srcTitle } = props; + const { + strings, + dispatch, + address, + src, + title, + srcTitle, + tooltip, + disabled = false, + } = props; const onSelection = (media: Media) => { props.onSelection(media); @@ -110,7 +123,7 @@ const MediaSelector = (props: MediaSelectorProps) => { uploading: true, }), ); - let res: any = await fetch(presignedUrl, { + const res = await fetch(presignedUrl, { method: "POST", body: fD, }); @@ -121,8 +134,8 @@ const MediaSelector = (props: MediaSelectorProps) => { } return media; } else { - res = await res.json(); - throw new Error(res.error); + const resp = await res.json(); + throw new Error(resp.error); } }; @@ -154,7 +167,9 @@ const MediaSelector = (props: MediaSelectorProps) => { try { setUploading(true); const fetch = new FetchBuilder() - .setUrl(`${address.backend}/api/media/${props.mediaId}`) + .setUrl( + `${address.backend}/api/media/${props.mediaId}/${props.type}`, + ) .setHttpMethod("DELETE") .setIsGraphQLEndpoint(false) .build(); @@ -174,34 +189,51 @@ const MediaSelector = (props: MediaSelectorProps) => { }; return ( -

-

{title}

-
- -

{srcTitle}

+
+ +
+ {!props.hidePreview && ( +
+ +

{srcTitle}

+
+ )} + {props.mediaId && ( + + {uploading + ? "Working..." + : strings.removeButtonCaption || "Remove media"} + + )}
- {props.mediaId && ( - - )} {!props.mediaId && (
+ {strings.buttonCaption || "Select media"} - + } open={dialogOpened} onOpenChange={setDialogOpened} okButton={ - + } > {error &&
{error}
}