feat: certification rules, content sanitization, handle draft course updates and reset content, better redis logging
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AssignAudienceCoursesRequest,
|
||||
DeleteAudienceMemberRequest,
|
||||
DeleteTeamRequest,
|
||||
DomainRequestRequest,
|
||||
GetAudienceRequest,
|
||||
@@ -8,6 +9,8 @@ import type {
|
||||
InviteTeamRequest,
|
||||
OrgPublicCourses,
|
||||
OrganizationAudience,
|
||||
OrganizationAudiencePagination,
|
||||
OrganizationAudienceQuery,
|
||||
OrganizationTeamMembers,
|
||||
ResendAudienceInviteRequest,
|
||||
RevokeAudienceInviteRequest
|
||||
@@ -36,6 +39,7 @@ import { resolve } from '$app/paths';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
import { DEFAULT_ORG_AUDIENCE_QUERY, toAudienceRequestQuery } from '../utils/audience-query-utils';
|
||||
|
||||
export interface TOrgUpdateForm {
|
||||
name?: string;
|
||||
@@ -59,9 +63,16 @@ export interface TOrgUpdateForm {
|
||||
class OrgApi extends BaseApiWithErrors {
|
||||
teamMembers = $state<OrganizationTeamMembers>([]);
|
||||
audience = $state<OrganizationAudience>([]);
|
||||
audiencePagination = $state<OrganizationAudiencePagination | null>(null);
|
||||
publicCourses: OrgPublicCourses = $state([]);
|
||||
|
||||
isFetchingOrgPublicCourses = $state(false);
|
||||
private activeAudienceRequestController: AbortController | null = null;
|
||||
|
||||
cancelAudienceRequest() {
|
||||
this.activeAudienceRequestController?.abort();
|
||||
this.activeAudienceRequestController = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets organization team members (non-students)
|
||||
@@ -91,16 +102,52 @@ class OrgApi extends BaseApiWithErrors {
|
||||
* @param orgId Organization ID
|
||||
* @returns Audience array
|
||||
*/
|
||||
async getOrgAudience(orgId?: string) {
|
||||
async getOrgAudience(
|
||||
orgId?: string,
|
||||
query?: Partial<OrganizationAudienceQuery>,
|
||||
options: { abortPrevious?: boolean; signal?: AbortSignal } = {}
|
||||
) {
|
||||
if (!orgId) return;
|
||||
|
||||
return this.execute<GetAudienceRequest>({
|
||||
requestFn: () => classroomio.organization.audience.$get(),
|
||||
const requestQuery = toAudienceRequestQuery({
|
||||
...DEFAULT_ORG_AUDIENCE_QUERY,
|
||||
...query
|
||||
});
|
||||
|
||||
let requestSignal = options.signal;
|
||||
let requestController: AbortController | null = null;
|
||||
|
||||
if (options.abortPrevious) {
|
||||
this.cancelAudienceRequest();
|
||||
requestController = new AbortController();
|
||||
this.activeAudienceRequestController = requestController;
|
||||
requestSignal = requestController.signal;
|
||||
}
|
||||
|
||||
const response = await this.execute<GetAudienceRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.audience.$get(
|
||||
{
|
||||
query: requestQuery!
|
||||
},
|
||||
{
|
||||
init: {
|
||||
signal: requestSignal
|
||||
}
|
||||
}
|
||||
),
|
||||
logContext: 'fetching organization audience',
|
||||
onSuccess: (response) => {
|
||||
this.audience = response.data;
|
||||
this.audiencePagination = response.pagination;
|
||||
}
|
||||
});
|
||||
|
||||
if (requestController && this.activeAudienceRequestController === requestController) {
|
||||
this.activeAudienceRequestController = null;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,7 +431,6 @@ class OrgApi extends BaseApiWithErrors {
|
||||
* Imports users into the organization as students
|
||||
*/
|
||||
async importAudienceMembers(data: TImportAudienceMembers) {
|
||||
console.log('importAudienceMembers', data);
|
||||
return this.execute<ImportAudienceRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.audience.import.$post({
|
||||
@@ -396,7 +442,8 @@ class OrgApi extends BaseApiWithErrors {
|
||||
snackbar.success(
|
||||
t.get('audience.import.snackbar_success', {
|
||||
imported: d.imported,
|
||||
skipped: d.skipped,
|
||||
assigned: d.assigned ?? 0,
|
||||
pendingInvitesRenewed: d.pendingInvitesRenewed ?? 0,
|
||||
emailsSent: d.emailsSent
|
||||
})
|
||||
);
|
||||
@@ -460,8 +507,6 @@ class OrgApi extends BaseApiWithErrors {
|
||||
} else {
|
||||
snackbar.error('audience.invite.resend_snackbar_email_failed');
|
||||
}
|
||||
const orgId = get(currentOrg).id;
|
||||
if (orgId) this.getOrgAudience(orgId);
|
||||
},
|
||||
onError: (result) => {
|
||||
if (typeof result === 'string') {
|
||||
@@ -480,8 +525,6 @@ class OrgApi extends BaseApiWithErrors {
|
||||
logContext: 'revoking audience invite',
|
||||
onSuccess: () => {
|
||||
snackbar.success('audience.invite.revoke_snackbar_success');
|
||||
const orgId = get(currentOrg).id;
|
||||
if (orgId) this.getOrgAudience(orgId);
|
||||
},
|
||||
onError: (result) => {
|
||||
if (typeof result === 'string') {
|
||||
@@ -490,6 +533,27 @@ class OrgApi extends BaseApiWithErrors {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAudienceMember(memberId: number) {
|
||||
return this.execute<DeleteAudienceMemberRequest>({
|
||||
requestFn: () =>
|
||||
classroomio.organization.audience[':memberId'].$delete({
|
||||
param: { memberId: memberId.toString() }
|
||||
}),
|
||||
logContext: 'removing audience member',
|
||||
onSuccess: () => {
|
||||
snackbar.success('audience.delete.snackbar_success');
|
||||
},
|
||||
onError: (result) => {
|
||||
if (typeof result === 'string') {
|
||||
snackbar.error(result);
|
||||
return;
|
||||
}
|
||||
|
||||
snackbar.error('error' in result ? result.error : result.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const orgApi = new OrgApi();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { OrganizationAudienceQuery, OrganizationAudienceRequestQuery } from './types';
|
||||
|
||||
export const DEFAULT_ORG_AUDIENCE_QUERY: OrganizationAudienceQuery = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
sortBy: 'createdAt',
|
||||
sortOrder: 'desc'
|
||||
};
|
||||
|
||||
export function getAudienceQueryFromSearchParams(
|
||||
searchParams: URLSearchParams,
|
||||
defaults: OrganizationAudienceQuery = DEFAULT_ORG_AUDIENCE_QUERY
|
||||
): OrganizationAudienceQuery {
|
||||
const page = Number(searchParams.get('page'));
|
||||
const limit = Number(searchParams.get('limit'));
|
||||
const sortBy = searchParams.get('sortBy');
|
||||
const sortOrder = searchParams.get('sortOrder');
|
||||
const search = searchParams.get('search')?.trim() || undefined;
|
||||
|
||||
return {
|
||||
page: Number.isFinite(page) && page > 0 ? Math.floor(page) : defaults.page,
|
||||
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : defaults.limit,
|
||||
sortBy: sortBy === 'name' || sortBy === 'email' || sortBy === 'createdAt' ? sortBy : defaults.sortBy,
|
||||
sortOrder: sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : defaults.sortOrder,
|
||||
search
|
||||
};
|
||||
}
|
||||
|
||||
export function getAudienceSearchParams(query: OrganizationAudienceQuery): URLSearchParams {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('page', String(query.page));
|
||||
searchParams.set('limit', String(query.limit));
|
||||
searchParams.set('sortBy', query.sortBy);
|
||||
searchParams.set('sortOrder', query.sortOrder);
|
||||
|
||||
if (query.search) {
|
||||
searchParams.set('search', query.search);
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
export function toAudienceRequestQuery(
|
||||
query?: Partial<OrganizationAudienceQuery>
|
||||
): OrganizationAudienceRequestQuery | undefined {
|
||||
if (!query) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
page: query.page != null ? String(query.page) : undefined,
|
||||
limit: query.limit != null ? String(query.limit) : undefined,
|
||||
search: query.search,
|
||||
sortBy: query.sortBy,
|
||||
sortOrder: query.sortOrder
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
import { classroomio, type InferRequestType, type InferResponseType } from '$lib/utils/services/api';
|
||||
|
||||
// Team member types
|
||||
export type GetTeamRequest = (typeof classroomio.organization)['team']['$get'];
|
||||
@@ -20,9 +20,19 @@ export type OrganizationTeamMembers = Array<
|
||||
export type GetAudienceRequest = (typeof classroomio.organization)['audience']['$get'];
|
||||
export type OrganizationAudienceResponse = InferResponseType<GetAudienceRequest> | null;
|
||||
export type OrganizationAudienceSuccess = Extract<InferResponseType<GetAudienceRequest>, { success: true }>;
|
||||
export type OrganizationAudienceRequestQuery = NonNullable<InferRequestType<GetAudienceRequest>['query']>;
|
||||
export type OrganizationAudienceSortBy = 'createdAt' | 'name' | 'email';
|
||||
export type OrganizationAudienceSortOrder = 'asc' | 'desc';
|
||||
export type OrganizationAudienceQuery = {
|
||||
page: number;
|
||||
limit: number;
|
||||
search?: string;
|
||||
sortBy: OrganizationAudienceSortBy;
|
||||
sortOrder: OrganizationAudienceSortOrder;
|
||||
};
|
||||
|
||||
export type OrganizationAudience = OrganizationAudienceSuccess['data'];
|
||||
|
||||
export type OrganizationAudiencePagination = OrganizationAudienceSuccess['pagination'];
|
||||
export type OrganizationAudienceMember = OrganizationAudience[number];
|
||||
|
||||
// Org Public Courses types
|
||||
@@ -43,6 +53,8 @@ export type InviteTeamData = InviteTeamSuccess['data'];
|
||||
|
||||
export type DeleteTeamRequest = (typeof classroomio.organization)['team'][':memberId']['$delete'];
|
||||
export type DeleteTeamSuccess = Extract<InferResponseType<DeleteTeamRequest>, { success: true }>;
|
||||
export type DeleteAudienceMemberRequest = (typeof classroomio.organization)['audience'][':memberId']['$delete'];
|
||||
export type DeleteAudienceMemberSuccess = Extract<InferResponseType<DeleteAudienceMemberRequest>, { success: true }>;
|
||||
|
||||
// update organization types
|
||||
export type UpdateOrganizationRequest = (typeof classroomio.organization)['$put'];
|
||||
|
||||
Reference in New Issue
Block a user