diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/AdvancedStudentOptionsDialog.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/AdvancedStudentOptionsDialog.js new file mode 100644 index 000000000000..c83f41d9b245 --- /dev/null +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/AdvancedStudentOptionsDialog.js @@ -0,0 +1,78 @@ +// @flow + +import * as React from 'react'; +import { I18n } from '@lingui/react'; +import { t, Trans } from '@lingui/macro'; +import Dialog from '../../../../UI/Dialog'; +import FlatButton from '../../../../UI/FlatButton'; +import { CompactToggleField } from '../../../../UI/CompactToggleField'; +import { Line } from '../../../../UI/Grid'; +import TeamContext from '../../../../Profile/Team/TeamContext'; + +type Props = {| + onClose: () => void, +|}; + +const AdvancedStudentOptionsDialog = ({ onClose }: Props): React.Node => { + const { team, onUpdateTeam } = React.useContext(TeamContext); + const [isUpdatingTeam, setIsUpdatingTeam] = React.useState(false); + + const onToggleStudentsAskAi = React.useCallback( + async (allowed: boolean) => { + setIsUpdatingTeam(true); + try { + await onUpdateTeam({ classrooms: { hideAskAi: !allowed } }); + } catch (error) { + console.error( + 'An error occurred while updating the team AI setting:', + error + ); + } finally { + setIsUpdatingTeam(false); + } + }, + [onUpdateTeam] + ); + + return ( + + {({ i18n }) => ( + Advanced student options} + actions={[ + Close} + disabled={isUpdatingTeam} + key="close" + primary={false} + onClick={onClose} + />, + ]} + maxWidth="sm" + cannotBeDismissed={isUpdatingTeam} + onRequestClose={onClose} + open + > + + { + onToggleStudentsAskAi(allowed); + }} + disabled={isUpdatingTeam} + /> + + + )} + + ); +}; + +export default AdvancedStudentOptionsDialog; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/index.js index a5e6057576f5..1d9817443db6 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/index.js @@ -46,7 +46,10 @@ import { EducationCard } from '../LearnSection/EducationCard'; import UserSVG from '../../../../UI/CustomSvgIcons/User'; import { copyTextToClipboard } from '../../../../Utils/Clipboard'; import ManageEducationAccountDialog from './ManageEducationAccountDialog'; +import AdvancedStudentOptionsDialog from './AdvancedStudentOptionsDialog'; import TeamAvailableSeats from './TeamAvailableSeats'; +import IconButton from '../../../../UI/IconButton'; +import Settings from '../../../../UI/CustomSvgIcons/Settings'; const PADDING = 16; @@ -112,6 +115,10 @@ const TeamSection = React.forwardRef( manageSeatsDialogOpen, setManageSeatsDialogOpen, ] = React.useState(false); + const [ + advancedStudentOptionsDialogOpen, + setAdvancedStudentOptionsDialogOpen, + ] = React.useState(false); const forceUpdate = useForceUpdate(); const { isMobile } = useResponsiveWindowSize(); const contextMenu = React.useRef(null); @@ -296,14 +303,23 @@ const TeamSection = React.forwardRef( justifyContent="space-between" > - Manage : Manage seats - } - icon={} - onClick={() => setManageSeatsDialogOpen(true)} - /> + + Manage : Manage seats + } + icon={} + onClick={() => setManageSeatsDialogOpen(true)} + /> + setAdvancedStudentOptionsDialogOpen(true)} + > + + + ); @@ -540,6 +556,11 @@ const TeamSection = React.forwardRef( onClose={() => setManageSeatsDialogOpen(false)} /> )} + {advancedStudentOptionsDialogOpen && ( + setAdvancedStudentOptionsDialogOpen(false)} + /> + )} ); } diff --git a/newIDE/app/src/Profile/Team/TeamContext.js b/newIDE/app/src/Profile/Team/TeamContext.js index 23117775f5ee..4266a064107b 100644 --- a/newIDE/app/src/Profile/Team/TeamContext.js +++ b/newIDE/app/src/Profile/Team/TeamContext.js @@ -37,6 +37,9 @@ export type TeamState = {| newPassword: string ) => Promise, onEditUser: (editedUserId: string, changes: EditUserChanges) => Promise, + onUpdateTeam: (attributes: {| + classrooms: {| hideAskAi: boolean |}, + |}) => Promise, |}; export const initialTeamState = { @@ -61,6 +64,7 @@ export const initialTeamState = { onSetMember: async () => {}, onChangeMemberPassword: async () => {}, onEditUser: async () => {}, + onUpdateTeam: async () => {}, }; const TeamContext: React.Context = React.createContext( diff --git a/newIDE/app/src/Profile/Team/TeamProvider.js b/newIDE/app/src/Profile/Team/TeamProvider.js index b5a6911e4250..aef7e5b3cf7d 100644 --- a/newIDE/app/src/Profile/Team/TeamProvider.js +++ b/newIDE/app/src/Profile/Team/TeamProvider.js @@ -24,6 +24,7 @@ import { setUserAsMember, listTeamInvitations, editUser, + updateTeam, type EditUserChanges, } from '../../Utils/GDevelopServices/User'; import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext'; @@ -399,6 +400,20 @@ const TeamProvider = ({ children }: Props): React.Node => { [team, getAuthorizationHeader, adminUserId] ); + const onUpdateTeam = React.useCallback( + async (attributes: {| classrooms: {| hideAskAi: boolean |} |}) => { + if (!adminUserId || !team) return; + const updatedTeam = await updateTeam( + getAuthorizationHeader, + adminUserId, + team.id, + attributes + ); + setTeam(updatedTeam); + }, + [team, getAuthorizationHeader, adminUserId] + ); + const getAvailableSeats = React.useCallback( () => team && members && admins && invitations @@ -434,6 +449,7 @@ const TeamProvider = ({ children }: Props): React.Node => { onActivateMembers, onSetAdmin, onSetMember, + onUpdateTeam, }} > {children} diff --git a/newIDE/app/src/Utils/GDevelopServices/User.js b/newIDE/app/src/Utils/GDevelopServices/User.js index 79573b843e53..fd87c179848a 100644 --- a/newIDE/app/src/Utils/GDevelopServices/User.js +++ b/newIDE/app/src/Utils/GDevelopServices/User.js @@ -181,7 +181,16 @@ export type User = {| +password?: ?string, |}; -export type Team = {| id: string, createdAt: number, seats: number |}; +export type Team = {| + id: string, + createdAt: number, + seats: number, + /** + * Overrides applied to the `classrooms` capability of the students of this + * team. Keys mirror the `classrooms` capability of the limits. + */ + classrooms?: ?{| hideAskAi: boolean |}, +|}; export type TeamGroup = {| id: string, name: string |}; export type TeamInvitation = {| teamId: string, @@ -345,6 +354,24 @@ export const updateGroup = async ( }); }; +export const updateTeam = async ( + getAuthorizationHeader: () => Promise, + userId: string, + teamId: string, + attributes: {| classrooms: {| hideAskAi: boolean |} |} +): Promise => { + const authorizationHeader = await getAuthorizationHeader(); + const response = await client.patch(`/team/${teamId}`, attributes, { + headers: { Authorization: authorizationHeader }, + params: { userId }, + }); + return ensureObjectHasProperty({ + data: response.data, + propertyName: 'id', + endpointName: '/team/{id} of User API', + }); +}; + export const createGroup = async ( getAuthorizationHeader: () => Promise, userId: string, diff --git a/newIDE/app/src/stories/MockTeamProvider.js b/newIDE/app/src/stories/MockTeamProvider.js index 9f5142106601..1e288ca39904 100644 --- a/newIDE/app/src/stories/MockTeamProvider.js +++ b/newIDE/app/src/stories/MockTeamProvider.js @@ -195,7 +195,10 @@ export const MockTeamProvider = ({ noActiveMembers?: boolean, teamSize?: number, |}): React.Node => { - const team = { ...initialTeam, seats: teamSize || initialTeam.seats }; + const [team, setTeam] = React.useState({ + ...initialTeam, + seats: teamSize || initialTeam.seats, + }); const [nameChangeTryCount, setNameChangeTryCount] = React.useState(0); const [userChangeTryCount, setUserChangeTryCount] = React.useState(0); const [members, setMembers] = React.useState( @@ -507,6 +510,13 @@ export const MockTeamProvider = ({ onEditUser: editUser, invitations: null, onRefreshInvitations: async () => {}, + onUpdateTeam: async attributes => { + action('updateTeam')(attributes); + setTeam(currentTeam => ({ + ...currentTeam, + classrooms: attributes.classrooms, + })); + }, }} > diff --git a/newIDE/app/src/stories/componentStories/HomePage/TeamSection/AdvancedStudentOptionsDialog.stories.js b/newIDE/app/src/stories/componentStories/HomePage/TeamSection/AdvancedStudentOptionsDialog.stories.js new file mode 100644 index 000000000000..b6e666ee2c90 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/HomePage/TeamSection/AdvancedStudentOptionsDialog.stories.js @@ -0,0 +1,24 @@ +// @flow +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; + +import paperDecorator from '../../../PaperDecorator'; +import FixedHeightFlexContainer from '../../../FixedHeightFlexContainer'; +import AdvancedStudentOptionsDialog from '../../../../MainFrame/EditorContainers/HomePage/TeamSection/AdvancedStudentOptionsDialog'; +import { MockTeamProvider } from '../../../MockTeamProvider'; + +export default { + title: 'HomePage/TeamSection/AdvancedStudentOptionsDialog', + component: AdvancedStudentOptionsDialog, + decorators: [paperDecorator], +}; + +export const Default = (): React.Node => { + return ( + + + + + + ); +};