diff --git a/services/cms/src/utils/Gutenberg/editBlockThemeJsonSettings.ts b/services/cms/src/utils/Gutenberg/editBlockThemeJsonSettings.ts index c4a00f37653..568ce439757 100644 --- a/services/cms/src/utils/Gutenberg/editBlockThemeJsonSettings.ts +++ b/services/cms/src/utils/Gutenberg/editBlockThemeJsonSettings.ts @@ -1,8 +1,8 @@ export default function editBlockThemeJsonSettings( settingValue: unknown, - _settingName: string, + settingName: string, _clientId: string, - _blockName: string, + blockName: string, ): unknown { // Here we can turn on theme.json settings for the block editor diff --git a/services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx b/services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx index 5f8345eabcd..4b581a81e32 100644 --- a/services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx +++ b/services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx @@ -1,5 +1,5 @@ import { css } from "@emotion/css" -import { useContext } from "react" +import { useContext, useMemo } from "react" import { BlockRendererProps } from "../.." import { @@ -20,7 +20,7 @@ interface ExtraAttributes { const TableBlock: React.FC< React.PropsWithChildren> -> = ({ data }) => { +> = ({ data, dontAllowBlockToBeWiderThanContainerWidth }) => { const { hasFixedLayout, caption, @@ -32,6 +32,12 @@ const TableBlock: React.FC< const head = data.attributes.head const foot = data.attributes.foot + const hasManyColumns = useMemo( + () => body.length > 0 && body[0].cells && body[0].cells.length > 5, + [body], + ) + const shouldUseSmallerFont = hasManyColumns && dontAllowBlockToBeWiderThanContainerWidth + const { terms } = useContext(GlossaryContext) const isStriped = className === "is-style-stripes" @@ -46,7 +52,11 @@ const TableBlock: React.FC< } return ( -
+
{head && ( diff --git a/services/course-material/src/components/forms/SelectMarketingConsentForm.tsx b/services/course-material/src/components/forms/SelectMarketingConsentForm.tsx index bb2fb9fba0d..d39556562b3 100644 --- a/services/course-material/src/components/forms/SelectMarketingConsentForm.tsx +++ b/services/course-material/src/components/forms/SelectMarketingConsentForm.tsx @@ -1,9 +1,14 @@ import { useQuery } from "@tanstack/react-query" import { t } from "i18next" -import React, { useEffect, useState } from "react" +import React, { useEffect, useMemo, useState } from "react" -import { fetchUserMarketingConsent } from "@/services/backend" +import { + fetchCustomPrivacyPolicyCheckboxTexts, + fetchUserMarketingConsent, +} from "@/services/backend" +import ErrorBanner from "@/shared-module/common/components/ErrorBanner" import CheckBox from "@/shared-module/common/components/InputFields/CheckBox" +import Spinner from "@/shared-module/common/components/Spinner" import { assertNotNullOrUndefined } from "@/shared-module/common/utils/nullability" interface SelectMarketingConsentFormProps { @@ -20,12 +25,17 @@ const SelectMarketingConsentForm: React.FC = ({ const [marketingConsent, setMarketingConsent] = useState(false) const [emailSubscriptionConsent, setEmailSubscriptionConsent] = useState(false) - const fetchInitialMarketingConsent = useQuery({ + const initialMarketingConsentQuery = useQuery({ queryKey: ["marketing-consent", courseId], queryFn: () => fetchUserMarketingConsent(assertNotNullOrUndefined(courseId)), enabled: courseId !== undefined, }) + const customPrivacyPolicyCheckboxTextsQuery = useQuery({ + queryKey: ["customPrivacyPolicyCheckboxTexts", courseId], + queryFn: () => fetchCustomPrivacyPolicyCheckboxTexts(courseId), + }) + const handleEmailSubscriptionConsentChange = (event: React.ChangeEvent) => { const isChecked = event.target.checked onEmailSubscriptionConsentChange(isChecked) @@ -39,24 +49,62 @@ const SelectMarketingConsentForm: React.FC = ({ } useEffect(() => { - if (fetchInitialMarketingConsent.isSuccess) { - setMarketingConsent(fetchInitialMarketingConsent.data.consent) + if (initialMarketingConsentQuery.isSuccess) { + setMarketingConsent(initialMarketingConsentQuery.data.consent) const emailSub = - fetchInitialMarketingConsent.data.email_subscription_in_mailchimp === "subscribed" + initialMarketingConsentQuery.data.email_subscription_in_mailchimp === "subscribed" setEmailSubscriptionConsent(emailSub) } - }, [fetchInitialMarketingConsent.data, fetchInitialMarketingConsent.isSuccess]) + }, [initialMarketingConsentQuery.data, initialMarketingConsentQuery.isSuccess]) + + const marketingConsentCheckboxText = useMemo(() => { + if (customPrivacyPolicyCheckboxTextsQuery.isSuccess) { + const customText = customPrivacyPolicyCheckboxTextsQuery.data.find( + (text) => text.text_slug === "marketing-consent", + ) + if (customText) { + return customText.text_html + } + } + return t("marketing-consent-checkbox-text") + }, [customPrivacyPolicyCheckboxTextsQuery.data, customPrivacyPolicyCheckboxTextsQuery.isSuccess]) + + const marketingConsentPrivacyPolicyCheckboxText = useMemo(() => { + if (customPrivacyPolicyCheckboxTextsQuery.isSuccess) { + const customText = customPrivacyPolicyCheckboxTextsQuery.data.find( + (text) => text.text_slug === "privacy-policy", + ) + if (customText) { + return customText.text_html + } + } + return t("marketing-consent-privacy-policy-checkbox-text") + }, [customPrivacyPolicyCheckboxTextsQuery.data, customPrivacyPolicyCheckboxTextsQuery.isSuccess]) + + if (initialMarketingConsentQuery.isLoading || customPrivacyPolicyCheckboxTextsQuery.isLoading) { + return + } + if (initialMarketingConsentQuery.isError || customPrivacyPolicyCheckboxTextsQuery.isError) { + return ( + + ) + } return ( <> const response = await courseMaterialClient.get(`/courses/${courseId}/privacy-link`) return validateResponse(response, isArray(isPrivacyLink)) } + +export const fetchCustomPrivacyPolicyCheckboxTexts = async ( + courseId: string, +): Promise => { + const response = await courseMaterialClient.get( + `/courses/${courseId}/custom-privacy-policy-checkbox-texts`, + { responseType: "json" }, + ) + return validateResponse(response, isArray(isCourseCustomPrivacyPolicyCheckboxText)) +} diff --git a/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.down.sql b/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.down.sql new file mode 100644 index 00000000000..c2e7b899e04 --- /dev/null +++ b/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.down.sql @@ -0,0 +1 @@ +DROP TABLE course_custom_privacy_policy_checkbox_texts; diff --git a/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.up.sql b/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.up.sql new file mode 100644 index 00000000000..86f2b0ddbf6 --- /dev/null +++ b/services/headless-lms/migrations/20241212140953_course_custom_privacy_policy_checkbox_texts.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE course_custom_privacy_policy_checkbox_texts ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + course_id UUID NOT NULL REFERENCES courses(id), + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMP WITH TIME ZONE, + text_html TEXT NOT NULL, + text_slug TEXT NOT NULL, + UNIQUE NULLS NOT DISTINCT (course_id, text_slug, deleted_at) +); +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON course_custom_privacy_policy_checkbox_texts FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); +COMMENT ON TABLE course_custom_privacy_policy_checkbox_texts IS 'Used to set the privacy policy checkbox texts in the course settings dialog when a course has a different privacy policy than all the other courses. (e.g., the Elements of AI course)'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.text_html IS 'The HTML content of the text.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.text_slug IS 'An identifier for the text, used to reference it in the course settings dialog.'; +COMMENT ON COLUMN course_custom_privacy_policy_checkbox_texts.course_id IS 'The course in which the text is shown.'; + +CREATE INDEX course_custom_privacy_policy_checkbox_texts_course_id_idx ON course_custom_privacy_policy_checkbox_texts (course_id) +WHERE deleted_at IS NULL; diff --git a/services/headless-lms/models/.sqlx/query-953607b07ff76b32b28eb3fb706a5770aa7fa66cc978781a8fc8235ff277eb08.json b/services/headless-lms/models/.sqlx/query-953607b07ff76b32b28eb3fb706a5770aa7fa66cc978781a8fc8235ff277eb08.json new file mode 100644 index 00000000000..5f9c2883427 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-953607b07ff76b32b28eb3fb706a5770aa7fa66cc978781a8fc8235ff277eb08.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM course_custom_privacy_policy_checkbox_texts\nWHERE course_id = $1\n AND deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "text_html", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "text_slug", + "type_info": "Text" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, false, true, false, false] + }, + "hash": "953607b07ff76b32b28eb3fb706a5770aa7fa66cc978781a8fc8235ff277eb08" +} diff --git a/services/headless-lms/models/src/course_custom_privacy_policy_checkbox_texts.rs b/services/headless-lms/models/src/course_custom_privacy_policy_checkbox_texts.rs new file mode 100644 index 00000000000..85ed82f9b0c --- /dev/null +++ b/services/headless-lms/models/src/course_custom_privacy_policy_checkbox_texts.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CourseCustomPrivacyPolicyCheckboxText { + pub id: Uuid, + pub course_id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub text_html: String, + pub text_slug: String, +} + +pub async fn get_all_by_course_id( + conn: &mut PgConnection, + course_id: Uuid, +) -> ModelResult> { + let texts = sqlx::query_as!( + CourseCustomPrivacyPolicyCheckboxText, + r#" +SELECT * +FROM course_custom_privacy_policy_checkbox_texts +WHERE course_id = $1 + AND deleted_at IS NULL; + "#, + course_id, + ) + .fetch_all(conn) + .await?; + Ok(texts) +} diff --git a/services/headless-lms/models/src/lib.rs b/services/headless-lms/models/src/lib.rs index d82dc0926a3..751eed17e47 100644 --- a/services/headless-lms/models/src/lib.rs +++ b/services/headless-lms/models/src/lib.rs @@ -17,6 +17,7 @@ pub mod code_giveaway_codes; pub mod code_giveaways; pub mod course_background_question_answers; pub mod course_background_questions; +pub mod course_custom_privacy_policy_checkbox_texts; pub mod course_exams; pub mod course_instance_enrollments; pub mod course_instances; diff --git a/services/headless-lms/server/src/controllers/course_material/courses.rs b/services/headless-lms/server/src/controllers/course_material/courses.rs index 20986c2b8b1..36a5ccd58c3 100644 --- a/services/headless-lms/server/src/controllers/course_material/courses.rs +++ b/services/headless-lms/server/src/controllers/course_material/courses.rs @@ -6,7 +6,10 @@ use actix_http::header::{self, X_FORWARDED_FOR}; use actix_web::web::Json; use chrono::Utc; use futures::{future::OptionFuture, FutureExt}; -use headless_lms_models::marketing_consents::UserMarketingConsent; +use headless_lms_models::{ + course_custom_privacy_policy_checkbox_texts::CourseCustomPrivacyPolicyCheckboxText, + marketing_consents::UserMarketingConsent, +}; use headless_lms_models::{partner_block::PartnersBlock, privacy_link::PrivacyLink}; use headless_lms_utils::ip_to_country::IpToCountryMapper; use isbot::Bots; @@ -1002,6 +1005,27 @@ async fn get_privacy_link( token.authorized_ok(web::Json(privacy_link)) } +/** +GET /courses/:course_id/custom-privacy-policy-checkbox-texts - Used to get customized checkbox texts for courses that use a different privacy policy than all our other courses (e.g. the Elements of AI course). These texts are shown in the course settings dialog. +*/ +#[instrument(skip(pool))] +async fn get_custom_privacy_policy_checkbox_texts( + course_id: web::Path, + pool: web::Data, + user: AuthUser, // Ensure the user is authenticated +) -> ControllerResult>> { + let mut conn = pool.acquire().await?; + + let token = authorize_access_to_course_material(&mut conn, Some(user.id), *course_id).await?; + + let texts = models::course_custom_privacy_policy_checkbox_texts::get_all_by_course_id( + &mut conn, *course_id, + ) + .await?; + + token.authorized_ok(web::Json(texts)) +} + /** Add a route for each controller in this module. @@ -1096,5 +1120,9 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { .route( "/{course_id}/fetch-user-marketing-consent", web::get().to(fetch_user_marketing_consent), + ) + .route( + "/{course_id}/custom-privacy-policy-checkbox-texts", + web::get().to(get_custom_privacy_policy_checkbox_texts), ); } diff --git a/services/headless-lms/server/src/ts_binding_generator.rs b/services/headless-lms/server/src/ts_binding_generator.rs index a9c2632cd2d..11c2176bc11 100644 --- a/services/headless-lms/server/src/ts_binding_generator.rs +++ b/services/headless-lms/server/src/ts_binding_generator.rs @@ -60,13 +60,14 @@ fn models(target: &mut File) { chatbot_conversations::ChatbotConversationInfo, code_giveaway_codes::CodeGiveawayCode, code_giveaways::CodeGiveaway, - code_giveaways::NewCodeGiveaway, code_giveaways::CodeGiveawayStatus, + code_giveaways::NewCodeGiveaway, course_background_question_answers::CourseBackgroundQuestionAnswer, course_background_question_answers::NewCourseBackgroundQuestionAnswer, course_background_questions::CourseBackgroundQuestion, course_background_questions::CourseBackgroundQuestionType, course_background_questions::CourseBackgroundQuestionsAndAnswers, + course_custom_privacy_policy_checkbox_texts::CourseCustomPrivacyPolicyCheckboxText, course_instance_enrollments::CourseInstanceEnrollment, course_instance_enrollments::CourseInstanceEnrollmentsInfo, course_instances::ChapterScore, @@ -171,7 +172,6 @@ fn models(target: &mut File) { library::progressing::UserWithModuleCompletions, marketing_consents::UserMarketingConsent, - material_references::MaterialReference, material_references::NewMaterialReference, @@ -203,6 +203,8 @@ fn models(target: &mut File) { pages::PageSearchResult, pages::PageWithExercises, pages::SearchRequest, + partner_block::PartnerBlockNew, + partner_block::PartnersBlock, peer_or_self_review_configs::CmsPeerOrSelfReviewConfig, peer_or_self_review_configs::CmsPeerOrSelfReviewConfiguration, peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig, @@ -220,6 +222,7 @@ fn models(target: &mut File) { pending_roles::PendingRole, playground_examples::PlaygroundExample, playground_examples::PlaygroundExampleData, + privacy_link::PrivacyLink, proposed_block_edits::BlockProposal, proposed_block_edits::BlockProposalAction, proposed_block_edits::BlockProposalInfo, @@ -267,9 +270,6 @@ fn models(target: &mut File) { user_exercise_states::UserExerciseState, user_research_consents::UserResearchConsent, users::User, - privacy_link::PrivacyLink, - partner_block::PartnersBlock, - partner_block::PartnerBlockNew, }; } diff --git a/shared-module/packages/common/src/bindings.guard.ts b/shared-module/packages/common/src/bindings.guard.ts index f452e732ffc..aac2f690db5 100644 --- a/shared-module/packages/common/src/bindings.guard.ts +++ b/shared-module/packages/common/src/bindings.guard.ts @@ -50,6 +50,7 @@ import { CourseBackgroundQuestionType, CourseBreadcrumbInfo, CourseCount, + CourseCustomPrivacyPolicyCheckboxText, CourseExam, CourseInstance, CourseInstanceCompletionSummary, @@ -636,18 +637,6 @@ export function isCodeGiveaway(obj: unknown): obj is CodeGiveaway { ) } -export function isNewCodeGiveaway(obj: unknown): obj is NewCodeGiveaway { - const typedObj = obj as NewCodeGiveaway - return ( - ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["course_id"] === "string" && - typeof typedObj["name"] === "string" && - (typedObj["course_module_id"] === null || typeof typedObj["course_module_id"] === "string") && - (typedObj["require_course_specific_consent_form_question_id"] === null || - typeof typedObj["require_course_specific_consent_form_question_id"] === "string") - ) -} - export function isCodeGiveawayStatus(obj: unknown): obj is CodeGiveawayStatus { const typedObj = obj as CodeGiveawayStatus return ( @@ -664,6 +653,18 @@ export function isCodeGiveawayStatus(obj: unknown): obj is CodeGiveawayStatus { ) } +export function isNewCodeGiveaway(obj: unknown): obj is NewCodeGiveaway { + const typedObj = obj as NewCodeGiveaway + return ( + ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && + typeof typedObj["course_id"] === "string" && + typeof typedObj["name"] === "string" && + (typedObj["course_module_id"] === null || typeof typedObj["course_module_id"] === "string") && + (typedObj["require_course_specific_consent_form_question_id"] === null || + typeof typedObj["require_course_specific_consent_form_question_id"] === "string") + ) +} + export function isCourseBackgroundQuestionAnswer( obj: unknown, ): obj is CourseBackgroundQuestionAnswer { @@ -725,6 +726,22 @@ export function isCourseBackgroundQuestionsAndAnswers( ) } +export function isCourseCustomPrivacyPolicyCheckboxText( + obj: unknown, +): obj is CourseCustomPrivacyPolicyCheckboxText { + const typedObj = obj as CourseCustomPrivacyPolicyCheckboxText + return ( + ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && + typeof typedObj["id"] === "string" && + typeof typedObj["course_id"] === "string" && + typeof typedObj["created_at"] === "string" && + typeof typedObj["updated_at"] === "string" && + (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && + typeof typedObj["text_html"] === "string" && + typeof typedObj["text_slug"] === "string" + ) +} + export function isCourseInstanceEnrollment(obj: unknown): obj is CourseInstanceEnrollment { const typedObj = obj as CourseInstanceEnrollment return ( @@ -2652,6 +2669,26 @@ export function isSearchRequest(obj: unknown): obj is SearchRequest { ) } +export function isPartnerBlockNew(obj: unknown): obj is PartnerBlockNew { + const typedObj = obj as PartnerBlockNew + return ( + ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && + typeof typedObj["course_id"] === "string" + ) +} + +export function isPartnersBlock(obj: unknown): obj is PartnersBlock { + const typedObj = obj as PartnersBlock + return ( + ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && + typeof typedObj["id"] === "string" && + typeof typedObj["created_at"] === "string" && + typeof typedObj["updated_at"] === "string" && + (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && + typeof typedObj["course_id"] === "string" + ) +} + export function isCmsPeerOrSelfReviewConfig(obj: unknown): obj is CmsPeerOrSelfReviewConfig { const typedObj = obj as CmsPeerOrSelfReviewConfig return ( @@ -2891,6 +2928,20 @@ export function isPlaygroundExampleData(obj: unknown): obj is PlaygroundExampleD ) } +export function isPrivacyLink(obj: unknown): obj is PrivacyLink { + const typedObj = obj as PrivacyLink + return ( + ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && + typeof typedObj["id"] === "string" && + typeof typedObj["created_at"] === "string" && + typeof typedObj["updated_at"] === "string" && + (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && + typeof typedObj["title"] === "string" && + typeof typedObj["url"] === "string" && + typeof typedObj["course_id"] === "string" + ) +} + export function isBlockProposal(obj: unknown): obj is BlockProposal { const typedObj = obj as BlockProposal return ( @@ -3442,40 +3493,6 @@ export function isUser(obj: unknown): obj is User { ) } -export function isPrivacyLink(obj: unknown): obj is PrivacyLink { - const typedObj = obj as PrivacyLink - return ( - ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["id"] === "string" && - typeof typedObj["created_at"] === "string" && - typeof typedObj["updated_at"] === "string" && - (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && - typeof typedObj["title"] === "string" && - typeof typedObj["url"] === "string" && - typeof typedObj["course_id"] === "string" - ) -} - -export function isPartnersBlock(obj: unknown): obj is PartnersBlock { - const typedObj = obj as PartnersBlock - return ( - ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["id"] === "string" && - typeof typedObj["created_at"] === "string" && - typeof typedObj["updated_at"] === "string" && - (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && - typeof typedObj["course_id"] === "string" - ) -} - -export function isPartnerBlockNew(obj: unknown): obj is PartnerBlockNew { - const typedObj = obj as PartnerBlockNew - return ( - ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["course_id"] === "string" - ) -} - export function isUploadResult(obj: unknown): obj is UploadResult { const typedObj = obj as UploadResult return ( diff --git a/shared-module/packages/common/src/bindings.ts b/shared-module/packages/common/src/bindings.ts index b61b1e9cd14..2119388d83e 100644 --- a/shared-module/packages/common/src/bindings.ts +++ b/shared-module/packages/common/src/bindings.ts @@ -239,6 +239,12 @@ export interface CodeGiveaway { name: string } +export type CodeGiveawayStatus = + | { tag: "Disabled" } + | { tag: "NotEligible" } + | { tag: "Eligible"; codes_left: boolean } + | { tag: "AlreadyGottenCode"; given_code: string } + export interface NewCodeGiveaway { course_id: string name: string @@ -246,12 +252,6 @@ export interface NewCodeGiveaway { require_course_specific_consent_form_question_id: string | null } -export type CodeGiveawayStatus = - | { tag: "Disabled" } - | { tag: "NotEligible" } - | { tag: "Eligible"; codes_left: boolean } - | { tag: "AlreadyGottenCode"; given_code: string } - export interface CourseBackgroundQuestionAnswer { id: string created_at: string @@ -285,6 +285,16 @@ export interface CourseBackgroundQuestionsAndAnswers { answers: Array } +export interface CourseCustomPrivacyPolicyCheckboxText { + id: string + course_id: string + created_at: string + updated_at: string + deleted_at: string | null + text_html: string + text_slug: string +} + export interface CourseInstanceEnrollment { user_id: string course_id: string @@ -1451,6 +1461,20 @@ export interface SearchRequest { query: string } +export interface PartnerBlockNew { + course_id: string + content: unknown | null +} + +export interface PartnersBlock { + id: string + created_at: string + updated_at: string + deleted_at: string | null + content: unknown + course_id: string +} + export interface CmsPeerOrSelfReviewConfig { id: string course_id: string @@ -1606,6 +1630,16 @@ export interface PlaygroundExampleData { data: unknown } +export interface PrivacyLink { + id: string + created_at: string + updated_at: string + deleted_at: string | null + title: string + url: string + course_id: string +} + export type BlockProposal = | ({ type: "edited-block-still-exists" } & EditedBlockStillExistsData) | ({ type: "edited-block-no-longer-exists" } & EditedBlockNoLongerExistsData) @@ -1947,30 +1981,6 @@ export interface User { email_domain: string | null } -export interface PrivacyLink { - id: string - created_at: string - updated_at: string - deleted_at: string | null - title: string - url: string - course_id: string -} - -export interface PartnersBlock { - id: string - created_at: string - updated_at: string - deleted_at: string | null - content: unknown - course_id: string -} - -export interface PartnerBlockNew { - course_id: string - content: unknown | null -} - export interface UploadResult { url: string } diff --git a/shared-module/packages/common/src/locales/uk/course-material.json b/shared-module/packages/common/src/locales/uk/course-material.json index 7ec9bf0f13b..9e90687204e 100644 --- a/shared-module/packages/common/src/locales/uk/course-material.json +++ b/shared-module/packages/common/src/locales/uk/course-material.json @@ -93,7 +93,7 @@ "flip-card": "Перевернути картку", "generate-certicate": "Створити сертифікат", "generate-certificate-button-label": "Створити сертифікат для завершення", - "give-feedback": "Дайте відгук", + "give-feedback": "ЗАЛИШТЕ ВІДГУК", "glossary": "Глосарій", "grade": "Оцінка", "grading-failed": "Не вдалося оцінити вправу", @@ -183,7 +183,7 @@ "start-the-exam": "Почніть іспит!", "started-at-time": "Початок: {{time}}", "student-in-this-region": "Студенти, які навчаються в цих країнах", - "student-points": "Студентські бали", + "student-points": "Набрані бали", "submit-button": "Надіслати", "table-of-contents": "Виберіть сторінку в розділі", "template-exercise-instructions": "Ви можете надіслати свою відповідь на кожне запитання, щоб зберегти їх стан перед здачею іспиту. Ви побачите результати після того, як здасте іспит або закінчиться таймер.", diff --git a/shared-module/packages/common/src/locales/uk/main-frontend.json b/shared-module/packages/common/src/locales/uk/main-frontend.json index 5c477770eda..e5fc3b86218 100644 --- a/shared-module/packages/common/src/locales/uk/main-frontend.json +++ b/shared-module/packages/common/src/locales/uk/main-frontend.json @@ -115,8 +115,8 @@ "confirm-email-address-instructions-2": "Підтвердьте електронну адресу свого облікового запису mooc.fi.", "confirm-email-address-instructions-3": "Будь ласка, відкрийте свою електронну пошту та перейдіть за посиланням у листі, щоб підтвердити свою електронну адресу.", "confirm-enable-generating-certificates": "Ви впевнені, що бажаєте ввімкнути генерацію сертифікатів?", - "confirm-password": "Підтвердьте пароль", - "confirm-your-password": "Підтвердити пароль", + "confirm-password": "Підтвердження пароля", + "confirm-your-password": "Підтвердіть пароль", "could-not-find-course-course-instance-or-user-course-settings-for-enrollment": "Не вдалося знайти курс або екземпляр курсу або налаштування курсу користувача для цієї реєстрації.", "course": "Курс", "course-id": "Ідентифікатор курсу", @@ -128,7 +128,7 @@ "course-status-summary": "Підсумок про стан курсу", "course-version-selection": "Вибір версії курсу", "courses": "Курси", - "create-an-acount": "Створити аккаунт", + "create-an-acount": "Створити обліковий запис", "create-certificate-configuration": "Створення конфігурації сертифіката", "create-configuration": "Створення конфігурації", "create-course-duplicate": "Копіювати вміст з іншого курсу", @@ -182,8 +182,8 @@ "enter-course-code": "Введіть код курсу", "enter-first-name": "Введіть ім'я", "enter-last-name": "Введіть прізвище", - "enter-your-email": "Введіть свою електронну адресу", - "enter-your-password": "Введіть ваш пароль", + "enter-your-email": "Введіть електронну адресу", + "enter-your-password": "Введіть пароль", "error-cannot-load-with-the-given-inputs": "Неможливо завантажити з указаними введеннями", "error-cannot-render-exercise-task-missing-submission": "Неможливо відобразити завдання для вправи, бракує подання.", "error-cannot-render-exercise-task-missing-url": "Неможливо відобразити завдання вправи, відсутня url.", @@ -288,7 +288,7 @@ "hourly-submissions-visualization-tooltip": "Година: {{day}}
Подання: {{submissions}}", "image-alt-what-to-display-on-chapter": "Що відображати в розділі.", "image-alt-what-to-display-on-organization": "Що відобразити в організації.", - "incorrect-email-or-password": "Неправильна електронна пошта або пароль.", + "incorrect-email-or-password": "Неправильна електронна адреса або пароль.", "input-field-chapter-color": "Колір розділу", "instance-ended-at-time": "Примірник закінчився о: {{time}}", "instance-has-no-set-opening-time": "Примірник не має встановленого часу відкриття", @@ -323,7 +323,7 @@ "label-deadline": "Дедлайн", "label-default": "За замовчуванням", "label-delete-current-overlay-svg": "Видалити поточний накладений SVG", - "label-email": "Електронна пошта", + "label-email": "Електронна адреса", "label-enable-registering-completion-to-uh-open-university": "Увімкнути реєстрацію завершення навчання у Відкритий університет Гельсінського університету", "label-ends-at": "Закінчується о", "label-entered-peer-review-queue": "Увійшли в чергу рецензування", @@ -603,7 +603,7 @@ "set-threshold": "Встановити поріг", "show-ended-exams": "Показати завершені іспити", "show-iframe-borders": "Показати межі iframe", - "sign-in-if-you-have-an-account": "Ви вже маєте акаунт? Увійти.", + "sign-in-if-you-have-an-account": "Ви вже маєте обліковий запис? Увійти.", "sign-up-with-mooc-subtitle": "Цей курс використовує облікові записи mooc.fi. Якщо ви раніше відвідували курси mooc.fi, ви можете використовувати наявні облікові дані на сторінці входу. На цій сторінці ви можете створити новий обліковий запис, який працює в більшості курсів і сервісів mooc.fi.", "sort-by-email": "Сортувати за електронною поштою", "sort-by-name": "Сортувати за назвою", @@ -627,7 +627,7 @@ "suspected-student": "Підозрюваний студент", "swedish": "Шведська", "teacher-has-graded-this-manually": "Вчитель оцінив це вручну", - "teacher-in-charge-email": "Електронна пошта відповідального викладача", + "teacher-in-charge-email": "Електронна адреса відповідального викладача", "teacher-in-charge-name": "П.І.Б. відповідального викладача", "test-course": "Тестовий курс", "text-anchor": "Якір тексту", diff --git a/shared-module/packages/common/src/locales/uk/shared-module.json b/shared-module/packages/common/src/locales/uk/shared-module.json index dc827ee13b3..97d5dff21c2 100644 --- a/shared-module/packages/common/src/locales/uk/shared-module.json +++ b/shared-module/packages/common/src/locales/uk/shared-module.json @@ -29,7 +29,7 @@ "default-toast-success-title": "Успіх", "dropdown-menu": "Спадне меню", "editable": "Можна редагувати", - "email": "Електронна пошта", + "email": "Електронна адреса", "email-templates": "Шаблони електронних листів", "error-cannot-render-dynamic-content-missing-url": "Неможливо відобразити динамічний вміст: відсутня URL-адреса", "error-part-of-page-has-crashed-error": "Частина сторінки аварійно завершила роботу: {{error}}",