Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-2167 preferred tools for boards #5282

Merged
merged 29 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9a8caf7
add preferred tool endpoint
MBergCap Oct 9, 2024
229f358
add preferred tool uc
MBergCap Oct 9, 2024
a2cf75d
add apiProperty description
MBergCap Oct 9, 2024
3196352
add seed data
MBergCap Oct 9, 2024
7ce331d
add uc test
MBergCap Oct 9, 2024
fce2ea8
fix
MBergCap Oct 9, 2024
501ded0
fix
MBergCap Oct 9, 2024
6193289
add feature flag FEATURE_PREFERRED_CTL_TOOLS_ENABLED
MBergCap Oct 10, 2024
bf766f6
fix typo
MBergCap Oct 10, 2024
d2b41f9
change query parameter
MBergCap Oct 10, 2024
f4b026e
fix param
MBergCap Oct 11, 2024
51a2c0e
fix uc test
MBergCap Oct 11, 2024
41d2e54
add api test
MBergCap Oct 11, 2024
6f7f80c
fix test
MBergCap Oct 11, 2024
31eee28
Merge branch 'main' into N21-2167-preferred-tools-for-boards
MBergCap Oct 11, 2024
2751b18
fix test
MBergCap Oct 11, 2024
67e4c4f
change response
MBergCap Oct 11, 2024
aceedb2
adjust seed data
MBergCap Oct 11, 2024
1996d0e
fix test
MBergCap Oct 14, 2024
ce2457f
add seed data
MBergCap Oct 14, 2024
85cbef7
adjust seed data
MBergCap Oct 14, 2024
c4f9fac
fix imports
MBergCap Oct 15, 2024
6c0e121
fix PR Comment
MBergCap Oct 15, 2024
004a3dc
Merge branch 'main' into N21-2167-preferred-tools-for-boards
sdinkov Oct 15, 2024
3a96f97
fix import
MBergCap Oct 15, 2024
e849502
Merge remote-tracking branch 'origin/N21-2167-preferred-tools-for-boa…
MBergCap Oct 15, 2024
49c8ef3
Merge branch 'main' into N21-2167-preferred-tools-for-boards
IgorCapCoder Oct 21, 2024
a3ccb18
Merge branch 'main' into N21-2167-preferred-tools-for-boards
IgorCapCoder Oct 23, 2024
c7a163a
Merge branch 'main' into N21-2167-preferred-tools-for-boards
IgorCapCoder Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe(BoardNodeCopyService.name, () => {
CTL_TOOLS_RELOAD_TIME_MS: 0,
FILES_STORAGE__SERVICE_BASE_URL: '',
CTL_TOOLS__PREFERRED_TOOLS_LIMIT: 10,
FEATURE_PREFERRED_CTL_TOOLS_ENABLED: false,
};
let contextExternalToolService: DeepMocked<ContextExternalToolService>;
let copyHelperService: DeepMocked<CopyHelperService>;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/server/admin-api-server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const config: AdminApiServerConfig = {
FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean,
FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean,
CTL_TOOLS__PREFERRED_TOOLS_LIMIT: Configuration.get('CTL_TOOLS__PREFERRED_TOOLS_LIMIT') as number,
FEATURE_PREFERRED_CTL_TOOLS_ENABLED: Configuration.get('FEATURE_PREFERRED_CTL_TOOLS_ENABLED') as boolean,
TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION: Configuration.get(
'TEACHER_VISIBILITY_FOR_EXTERNAL_TEAM_INVITATION'
) as string,
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/modules/server/api/dto/config.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export class ConfigResponse {
@ApiProperty()
FEATURE_CTL_TOOLS_COPY_ENABLED: boolean;

@ApiProperty()
FEATURE_PREFERRED_CTL_TOOLS_ENABLED: boolean;

@ApiProperty()
FEATURE_SHOW_MIGRATION_WIZARD: boolean;

Expand Down Expand Up @@ -291,6 +294,7 @@ export class ConfigResponse {
this.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED;
this.FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_ROOMS_VIEW_ENABLED;
this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.FEATURE_CTL_TOOLS_COPY_ENABLED;
this.FEATURE_PREFERRED_CTL_TOOLS_ENABLED = config.FEATURE_PREFERRED_CTL_TOOLS_ENABLED;
this.FEATURE_SHOW_MIGRATION_WIZARD = config.FEATURE_SHOW_MIGRATION_WIZARD;
this.MIGRATION_WIZARD_DOCUMENTATION_LINK = config.MIGRATION_WIZARD_DOCUMENTATION_LINK;
this.FEATURE_TLDRAW_ENABLED = config.FEATURE_TLDRAW_ENABLED;
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/server/api/test/server.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('Server Controller (API)', () => {
'FEATURE_COURSE_SHARE',
'FEATURE_CTL_TOOLS_COPY_ENABLED',
'FEATURE_CTL_TOOLS_TAB_ENABLED',
'FEATURE_PREFERRED_CTL_TOOLS_ENABLED',
'FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION',
'FEATURE_ES_COLLECTIONS_ENABLED',
'FEATURE_EXTENSIONS_ENABLED',
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/modules/server/server.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ const config: ServerConfig = {
) as number,
CTL_TOOLS_BACKEND_URL: Configuration.get('PUBLIC_BACKEND_URL') as string,
FEATURE_CTL_TOOLS_COPY_ENABLED: Configuration.get('FEATURE_CTL_TOOLS_COPY_ENABLED') as boolean,
FEATURE_PREFERRED_CTL_TOOLS_ENABLED: Configuration.get('FEATURE_PREFERRED_CTL_TOOLS_ENABLED') as boolean,
CTL_TOOLS_RELOAD_TIME_MS: Configuration.get('CTL_TOOLS_RELOAD_TIME_MS') as number,
HOST: Configuration.get('HOST') as string,
FILES_STORAGE__SERVICE_BASE_URL: Configuration.get('FILES_STORAGE__SERVICE_BASE_URL') as string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
SchoolExternalToolConfigurationTemplateListResponse,
SchoolExternalToolConfigurationTemplateResponse,
ToolContextTypesListResponse,
PreferredToolListResponse,
} from '../dto';

describe('ToolConfigurationController (API)', () => {
Expand Down Expand Up @@ -743,4 +744,175 @@ describe('ToolConfigurationController (API)', () => {
});
});
});

describe('[GET] tools/preferred-tools', () => {
describe('when the user is not authorized', () => {
const setup = async () => {
const school: SchoolEntity = schoolEntityFactory.buildWithId();

const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin();

const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId({
isPreferred: true,
iconName: 'iconName',
});

const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
school,
tool: externalTool,
});

await em.persistAndFlush([school, adminUser, adminAccount, externalTool, schoolExternalTool]);
em.clear();

const loggedInClient: TestApiClient = await testApiClient.login(adminAccount);

return {
loggedInClient,
};
};

it('should return a unauthorized status', async () => {
const { loggedInClient } = await setup();

const response: Response = await loggedInClient.get(`/preferred-tools`);

expect(response.status).toEqual(HttpStatus.UNAUTHORIZED);
});
});

describe('when preferred tools are available for a context', () => {
const setup = async () => {
const school: SchoolEntity = schoolEntityFactory.buildWithId();

const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({ school }, [
Permission.CONTEXT_TOOL_ADMIN,
]);

const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId({
restrictToContexts: [],
isPreferred: true,
iconName: 'iconName',
});

const externalToolWithContextRestriction: ExternalToolEntity = externalToolEntityFactory.buildWithId({
restrictToContexts: [ToolContextType.COURSE],
isPreferred: true,
iconName: 'iconName',
});

const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
school,
tool: externalTool,
});

const schoolExternalTool2: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
school,
tool: externalToolWithContextRestriction,
});

await em.persistAndFlush([
school,
teacherUser,
teacherAccount,
externalTool,
externalToolWithContextRestriction,
schoolExternalTool,
schoolExternalTool2,
]);
em.clear();

const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount);

return {
externalTool,
externalToolWithContextRestriction,
schoolExternalTool,
schoolExternalTool2,
loggedInClient,
};
};

it('should return an array of preferred tools', async () => {
const {
externalTool,
externalToolWithContextRestriction,
schoolExternalTool,
schoolExternalTool2,
loggedInClient,
} = await setup();

const response: Response = await loggedInClient.get('/preferred-tools');

expect(response.body).toEqual<PreferredToolListResponse>({
data: [
{
schoolExternalToolId: schoolExternalTool.id,
name: externalTool.name,
iconName: 'iconName',
},
{
schoolExternalToolId: schoolExternalTool2.id,
name: externalToolWithContextRestriction.name,
iconName: 'iconName',
},
],
});
});

it('should not return the context restricted tool', async () => {
const { loggedInClient, externalTool, schoolExternalTool } = await setup();

const response: Response = await loggedInClient.get('/preferred-tools').query({ contextType: 'board-element' });

expect(response.body).toEqual<PreferredToolListResponse>({
data: [
{
schoolExternalToolId: schoolExternalTool.id,
name: externalTool.name,
iconName: 'iconName',
},
],
});
});
});

describe('when no preferred tools are available', () => {
const setup = async () => {
const school: SchoolEntity = schoolEntityFactory.buildWithId();

const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher({}, [
Permission.CONTEXT_TOOL_ADMIN,
]);

const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId({
isPreferred: false,
});

const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({
school,
tool: externalTool,
});

await em.persistAndFlush([teacherUser, school, teacherAccount, externalTool, schoolExternalTool]);
em.clear();

const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount);

return {
loggedInClient,
};
};

it('should return an empty array', async () => {
const { loggedInClient } = await setup();

const response: Response = await loggedInClient.get('/preferred-tools');

expect(response.body).toEqual<PreferredToolListResponse>({
data: [],
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './school-external-tool-id.params';
export * from './context-external-tool-id.params';
export { ExternalToolMediumParams } from './external-tool-medium.params';
export { ExternalToolBulkCreateParams } from './external-tool-bulk-create.params';
export * from './tool-context-type.params';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IsEnum, IsOptional } from 'class-validator';
sdinkov marked this conversation as resolved.
Show resolved Hide resolved
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ToolContextType } from '@modules/tool/common/enum';

export class ToolContextTypeParams {
@IsOptional()
@IsEnum(ToolContextType, { each: true })
@ApiPropertyOptional({
enum: ToolContextType,
enumName: 'ToolContextType',
description: 'Context types for tools',
})
contextType?: ToolContextType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export {
ExternalToolImportResultListResponse,
ExternalToolImportResultResponse,
} from './external-tool-import-result-response';
export * from './preferred-tool.response';
export * from './preferred-tool-list.response';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { PreferredToolResponse } from './preferred-tool.response';

export class PreferredToolListResponse {
@ApiProperty({ type: [PreferredToolResponse] })
data: PreferredToolResponse[];

constructor(data: PreferredToolResponse[]) {
this.data = data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';

export class PreferredToolResponse {
@ApiProperty({ type: String, description: 'Id of the school external tool' })
schoolExternalToolId: string;

@ApiProperty({ type: String, description: 'Name of the external tool' })
name: string;

@ApiProperty({
type: String,
description: 'Name of the icon to be rendered when displaying it as a preferred tool',
})
iconName: string;

constructor(configuration: PreferredToolResponse) {
this.schoolExternalToolId = configuration.schoolExternalToolId;
this.name = configuration.name;
this.iconName = configuration.iconName;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CurrentUser, ICurrentUser, JwtAuthentication } from '@infra/auth-guard';
import { Controller, Get, Param } from '@nestjs/common';
import { Controller, Get, Param, Query } from '@nestjs/common';
import {
ApiForbiddenResponse,
ApiFoundResponse,
Expand All @@ -22,6 +22,8 @@ import {
SchoolExternalToolIdParams,
SchoolIdParams,
ToolContextTypesListResponse,
PreferredToolListResponse,
ToolContextTypeParams,
} from './dto';

@ApiTags('Tool')
Expand Down Expand Up @@ -95,6 +97,29 @@ export class ToolConfigurationController {
return mapped;
}

@Get('preferred-tools')
@ApiForbiddenResponse()
@ApiOperation({ summary: 'Lists all preferred tools that can be added for a given context' })
@ApiOkResponse({
description: 'List of preferred tools for a context',
type: PreferredToolListResponse,
})
public async getPreferredToolsForContext(
@CurrentUser() currentUser: ICurrentUser,
@Query() context: ToolContextTypeParams
): Promise<PreferredToolListResponse> {
const preferedTools: ContextExternalToolTemplateInfo[] =
await this.externalToolConfigurationUc.getPreferedToolsForContext(
currentUser.userId,
currentUser.schoolId,
context.contextType
);

const mapped: PreferredToolListResponse = ToolConfigurationMapper.mapToPreferredToolListResponse(preferedTools);

return mapped;
}

@Get('school-external-tools/:schoolExternalToolId/configuration-template')
@ApiUnauthorizedResponse()
@ApiForbiddenResponse()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
SchoolExternalToolConfigurationTemplateListResponse,
SchoolExternalToolConfigurationTemplateResponse,
ToolContextTypesListResponse,
PreferredToolListResponse,
PreferredToolResponse,
} from '../controller/dto';
import { ExternalTool } from '../domain';
import { ContextExternalToolTemplateInfo } from '../uc';
Expand Down Expand Up @@ -77,4 +79,24 @@ export class ToolConfigurationMapper {

return mappedTypes;
}

static mapToPreferredToolListResponse(preferedTools: ContextExternalToolTemplateInfo[]): PreferredToolListResponse {
const mappedTools = preferedTools.map((tool): PreferredToolResponse => this.mapToPreferredToolResponse(tool));

const mapped = new PreferredToolListResponse(mappedTools);

return mapped;
}

static mapToPreferredToolResponse(preferredTool: ContextExternalToolTemplateInfo): PreferredToolResponse {
const { externalTool, schoolExternalTool } = preferredTool;

const mapped = new PreferredToolResponse({
schoolExternalToolId: schoolExternalTool.id ?? '',
name: externalTool.name,
iconName: externalTool.iconName ?? '',
});

return mapped;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,5 +151,6 @@ export const externalToolFactory = ExternalToolFactory.define(ExternalTool, ({ s
createdAt: new Date(2020, 1, 1),
restrictToContexts: undefined,
isPreferred: false,
iconName: undefined,
};
});
Loading
Loading