diff --git a/backend/src/common/actions.ts b/backend/src/common/actions.ts index bcf7de433..470c557a2 100644 --- a/backend/src/common/actions.ts +++ b/backend/src/common/actions.ts @@ -9,12 +9,9 @@ const actions = { ADD_POST_GROUP_SUCCESS: 'retrospected/group/add/success', DELETE_POST_GROUP: 'retrospected/group/delete', EDIT_POST_GROUP: 'retrospected/group/edit', - RENAME_SESSION: 'retrospected/session/rename', JOIN_SESSION: 'retrospected/session/join', LEAVE_SESSION: 'retrospected/session/leave', - EDIT_OPTIONS: 'retrospected/session/options/edit', - EDIT_COLUMNS: 'retrospected/session/columns/edit', - SAVE_TEMPLATE: 'retrospected/session/template/save', + SAVE_SESSION_SETTINGS: 'retrospected/session/settings/save', LOCK_SESSION: 'retrospected/session/lock', REQUEST_BOARD: 'retrospected/session/request', USER_READY: 'retrospected/user-ready', @@ -32,10 +29,8 @@ const actions = { RECEIVE_DELETE_POST_GROUP: 'retrospected/group/receive/delete', RECEIVE_EDIT_POST_GROUP: 'retrospected/group/receive/edit', RECEIVE_BOARD: 'retrospected/posts/receive-all', - RECEIVE_OPTIONS: 'retrospected/session/options/receive', - RECEIVE_COLUMNS: 'retrospected/session/columns/receive', + RECEIVE_SESSION_SETTINGS: 'retrospected/session/settings/receive', RECEIVE_CLIENT_LIST: 'retrospected/session/receive/client-list', - RECEIVE_SESSION_NAME: 'retrospected/session/receive/rename', RECEIVE_LOCK_SESSION: 'retrospected/session/receive/lock', RECEIVE_UNAUTHORIZED: 'retrospected/session/receive/unauthorized', RECEIVE_RATE_LIMITED: 'retrospected/rate-limited-error', diff --git a/backend/src/common/models.ts b/backend/src/common/models.ts index 9d9d99bb0..06c0f5bcd 100644 --- a/backend/src/common/models.ts +++ b/backend/src/common/models.ts @@ -17,12 +17,12 @@ export const defaultOptions: SessionOptions = { allowTimer: true, timerDuration: 15 * 60, readonlyOnTimerEnd: true, - restrictTitleEditToOwner: false, - restrictReorderingToOwner: false, - restrictGroupingToOwner: false, + restrictTitleEditToModerator: false, + restrictReorderingToModerator: false, + restrictGroupingToModerator: false, }; -export const defaultSession: Omit = { +export const defaultSession: Omit = { id: '', columns: [ { id: '', index: 0, label: '', type: 'well', color: '', icon: null }, diff --git a/backend/src/common/types.ts b/backend/src/common/types.ts index f229c9321..bc4a8d493 100644 --- a/backend/src/common/types.ts +++ b/backend/src/common/types.ts @@ -1,15 +1,21 @@ -export interface Session extends PostContainer, Entity { +export type SessionSettings = Partial; + +export interface AllSessionSettings { name: string | null; + moderator: User; + options: SessionOptions; + columns: ColumnDefinition[]; + locked: boolean; + timer: Date | null; +} + +export interface Session extends AllSessionSettings, PostContainer, Entity { posts: Post[]; groups: PostGroup[]; - columns: ColumnDefinition[]; messages: Message[]; - options: SessionOptions; encrypted: string | null; - locked: boolean; createdBy: User; ready: string[]; - timer: Date | null; demo: boolean; } @@ -60,9 +66,9 @@ export interface SessionOptions { allowGrouping: boolean; allowReordering: boolean; allowCancelVote: boolean; - restrictTitleEditToOwner: boolean; - restrictReorderingToOwner: boolean; - restrictGroupingToOwner: boolean; + restrictTitleEditToModerator: boolean; + restrictReorderingToModerator: boolean; + restrictGroupingToModerator: boolean; blurCards: boolean; newPostsFirst: boolean; allowTimer: boolean; @@ -211,6 +217,7 @@ export type TrackingEvent = | 'home/load-previous' | 'game/session/edit-options' | 'game/session/edit-columns' + | 'game/session/save-options' | 'game/session/reset' | 'game/session/disconnect' | 'game/session/unexpected-disconnection' diff --git a/backend/src/common/ws.ts b/backend/src/common/ws.ts index f879e36cd..1ab1ba687 100644 --- a/backend/src/common/ws.ts +++ b/backend/src/common/ws.ts @@ -1,8 +1,7 @@ import { - ColumnDefinition, Post, PostGroup, - SessionOptions, + SessionSettings, User, VoteExtract, VoteType, @@ -18,10 +17,6 @@ export interface WsUserData { user: User; } -export interface WsNameData { - name: string; -} - export interface WsPostUpdatePayload { post: Omit, 'user'>, 'group'>; groupId: string | null; @@ -58,9 +53,9 @@ export interface WsDeleteGroupPayload { groupId: string; } -export interface WsSaveTemplatePayload { - columns: ColumnDefinition[]; - options: SessionOptions; +export interface WsSaveSessionSettingsPayload { + settings: SessionSettings; + saveAsTemplate: boolean; } export interface WsUserReadyPayload { @@ -84,7 +79,8 @@ export type WsErrorType = | 'cannot_record_chat_message' | 'cannot_cancel_votes' | 'unknown_error' - | 'action_unauthorised'; + | 'action_unauthorised' + | 'cannot_save_session_settings'; export interface WsErrorPayload { type: WsErrorType; diff --git a/backend/src/db/actions/sessions.ts b/backend/src/db/actions/sessions.ts index ee73161bc..16a40ef90 100644 --- a/backend/src/db/actions/sessions.ts +++ b/backend/src/db/actions/sessions.ts @@ -372,6 +372,32 @@ export async function updateName( }); } +export async function updateModerator( + sessionId: string, + moderatorId: string +): Promise { + return await transaction(async (manager) => { + try { + const sessionRepository = manager.withRepository(SessionRepository); + const session = await sessionRepository.findOne({ + where: { id: sessionId }, + }); + if (session) { + await sessionRepository.save({ + ...session, + moderator: { + id: moderatorId, + }, + }); + return true; + } + return false; + } catch { + return false; + } + }); +} + export async function getSessionWithVisitors( sessionId: string ): Promise { diff --git a/backend/src/db/actions/users.ts b/backend/src/db/actions/users.ts index 1f522891e..ae2abd4b5 100644 --- a/backend/src/db/actions/users.ts +++ b/backend/src/db/actions/users.ts @@ -1,5 +1,5 @@ import { UserEntity, UserView } from '../entities/index.js'; -import { EntityManager, Not } from 'typeorm'; +import { EntityManager, In, Not } from 'typeorm'; import { UserIdentityRepository, UserRepository, @@ -88,6 +88,15 @@ export async function getUserViewInner( return user || null; } +export async function getRelatedUsers(userId: string): Promise { + return await transaction(async (manager) => { + const userRepository = manager.withRepository(UserRepository); + const ids = await userRepository.getRelatedUsersIds(userId); + const userViewRepository = manager.getRepository(UserView); + return userViewRepository.findBy({ id: In(ids) }); + }); +} + export async function getPasswordIdentity( username: string ): Promise { diff --git a/backend/src/db/entities/Session.ts b/backend/src/db/entities/Session.ts index 744cdf418..f610ab3fe 100644 --- a/backend/src/db/entities/Session.ts +++ b/backend/src/db/entities/Session.ts @@ -31,6 +31,9 @@ export default class SessionEntity { @ManyToOne(() => UserEntity, { eager: true, cascade: true, nullable: false }) @Index() public createdBy: UserEntity; + @ManyToOne(() => UserEntity, { eager: true, cascade: true, nullable: false }) + @Index() + public moderator: UserEntity; @OneToMany(() => PostEntity, (post) => post.session, { cascade: true, nullable: false, @@ -80,6 +83,7 @@ export default class SessionEntity { columns: this.columns === undefined ? [] : this.columns.map((c) => c.toJson()), createdBy: this.createdBy.toJson(), + moderator: this.moderator.toJson(), groups: this.groups === undefined ? [] : this.groups.map((g) => g.toJson()), id: this.id, @@ -105,6 +109,7 @@ export default class SessionEntity { this.id = id; this.name = name; this.createdBy = createdBy; + this.moderator = createdBy; this.options = new SessionOptionsEntity(options); this.encrypted = null; this.locked = false; diff --git a/backend/src/db/entities/SessionOptions.ts b/backend/src/db/entities/SessionOptions.ts index 37373f01e..7d8880a02 100644 --- a/backend/src/db/entities/SessionOptions.ts +++ b/backend/src/db/entities/SessionOptions.ts @@ -31,11 +31,11 @@ export default class SessionOptionsEntity { @Column({ default: true }) public allowTimer: boolean; @Column({ default: false }) - public restrictTitleEditToOwner: boolean; + public restrictTitleEditToModerator: boolean; @Column({ default: false }) - public restrictReorderingToOwner: boolean; + public restrictReorderingToModerator: boolean; @Column({ default: false }) - public restrictGroupingToOwner: boolean; + public restrictGroupingToModerator: boolean; @Column({ type: 'numeric', default: 15 * 60 }) public timerDuration: number; @Column({ default: true }) @@ -69,10 +69,12 @@ export default class SessionOptionsEntity { this.allowTimer = optionsWithDefault.allowTimer; this.timerDuration = optionsWithDefault.timerDuration; this.readonlyOnTimerEnd = optionsWithDefault.readonlyOnTimerEnd; - this.restrictTitleEditToOwner = optionsWithDefault.restrictTitleEditToOwner; - this.restrictReorderingToOwner = - optionsWithDefault.restrictReorderingToOwner; - this.restrictGroupingToOwner = optionsWithDefault.restrictGroupingToOwner; + this.restrictTitleEditToModerator = + optionsWithDefault.restrictTitleEditToModerator; + this.restrictReorderingToModerator = + optionsWithDefault.restrictReorderingToModerator; + this.restrictGroupingToModerator = + optionsWithDefault.restrictGroupingToModerator; } } diff --git a/backend/src/db/migrations/1689787853666-Moderator.ts b/backend/src/db/migrations/1689787853666-Moderator.ts new file mode 100644 index 000000000..1b25031de --- /dev/null +++ b/backend/src/db/migrations/1689787853666-Moderator.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Moderator1689787853666 implements MigrationInterface { + name = 'Moderator1689787853666' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" ADD "moderator_id" character varying`); + await queryRunner.query(`CREATE INDEX "IDX_7127e358f4f0bf8686d77af14c" ON "sessions" ("moderator_id") `); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd" FOREIGN KEY ("moderator_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`UPDATE "sessions" SET "moderator_id" = "created_by_id"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd"`); + await queryRunner.query(`DROP INDEX "public"."IDX_7127e358f4f0bf8686d77af14c"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "moderator_id"`); + } + +} diff --git a/backend/src/db/migrations/1689787951867-ModeratorNonNull.ts b/backend/src/db/migrations/1689787951867-ModeratorNonNull.ts new file mode 100644 index 000000000..0da47a92f --- /dev/null +++ b/backend/src/db/migrations/1689787951867-ModeratorNonNull.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ModeratorNonNull1689787951867 implements MigrationInterface { + name = 'ModeratorNonNull1689787951867' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_1ccf045da14e5350b26ee882592"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "created_by_id" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_1ccf045da14e5350b26ee882592" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_1ccf045da14e5350b26ee882592"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "created_by_id" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_1ccf045da14e5350b26ee882592" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/backend/src/db/migrations/1695539888278-RenameModerator.ts b/backend/src/db/migrations/1695539888278-RenameModerator.ts new file mode 100644 index 000000000..617e12d5c --- /dev/null +++ b/backend/src/db/migrations/1695539888278-RenameModerator.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RenameModerator1695539888278 implements MigrationInterface { + name = 'RenameModerator1695539888278' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_title_edit_to_moderator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_reordering_to_moderator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_grouping_to_moderator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_title_edit_to_moderator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_reordering_to_moderator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_grouping_to_moderator" boolean NOT NULL DEFAULT false`); + + await queryRunner.query(`UPDATE "templates" SET + options_restrict_title_edit_to_moderator = options_restrict_title_edit_to_owner, + options_restrict_reordering_to_moderator = options_restrict_reordering_to_owner, + options_restrict_grouping_to_moderator = options_restrict_grouping_to_owner + `); + await queryRunner.query(`UPDATE "sessions" SET + options_restrict_title_edit_to_moderator = options_restrict_title_edit_to_owner, + options_restrict_reordering_to_moderator = options_restrict_reordering_to_owner, + options_restrict_grouping_to_moderator = options_restrict_grouping_to_owner + `); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_title_edit_to_owner"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_reordering_to_owner"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_grouping_to_owner"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_title_edit_to_owner"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_reordering_to_owner"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_grouping_to_owner"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_1ccf045da14e5350b26ee882592"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "created_by_id" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_1ccf045da14e5350b26ee882592" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_1ccf045da14e5350b26ee882592"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "created_by_id" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_1ccf045da14e5350b26ee882592" FOREIGN KEY ("created_by_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_grouping_to_owner" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_reordering_to_owner" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_restrict_title_edit_to_owner" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_grouping_to_owner" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_reordering_to_owner" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "templates" ADD "options_restrict_title_edit_to_owner" boolean NOT NULL DEFAULT false`); + + await queryRunner.query(`UPDATE "templates" SET + options_restrict_title_edit_to_owner = options_restrict_title_edit_to_moderator, + options_restrict_reordering_to_owner = options_restrict_reordering_to_moderator, + options_restrict_grouping_to_owner = options_restrict_grouping_to_moderator + `); + await queryRunner.query(`UPDATE "sessions" SET + options_restrict_title_edit_to_owner = options_restrict_title_edit_to_moderator, + options_restrict_reordering_to_owner = options_restrict_reordering_to_moderator, + options_restrict_grouping_to_owner = options_restrict_grouping_to_moderator + `); + + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_grouping_to_moderator"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_reordering_to_moderator"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_restrict_title_edit_to_moderator"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_grouping_to_moderator"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_reordering_to_moderator"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_restrict_title_edit_to_moderator"`); + + } + +} diff --git a/backend/src/db/migrations/1695540515122-ModeratorNonNull.ts b/backend/src/db/migrations/1695540515122-ModeratorNonNull.ts new file mode 100644 index 000000000..de7101fce --- /dev/null +++ b/backend/src/db/migrations/1695540515122-ModeratorNonNull.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ModeratorNonNull1695540515122 implements MigrationInterface { + name = 'ModeratorNonNull1695540515122' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "moderator_id" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd" FOREIGN KEY ("moderator_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd"`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "moderator_id" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "sessions" ADD CONSTRAINT "FK_7127e358f4f0bf8686d77af14cd" FOREIGN KEY ("moderator_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/backend/src/db/repositories/SessionRepository.ts b/backend/src/db/repositories/SessionRepository.ts index f0684700f..bfa72797a 100644 --- a/backend/src/db/repositories/SessionRepository.ts +++ b/backend/src/db/repositories/SessionRepository.ts @@ -26,7 +26,7 @@ export default getBaseRepository(SessionEntity).extend({ } }, async saveFromJson( - session: Omit, + session: Omit, authorId: string ): Promise { const sessionWithoutPosts = { @@ -34,6 +34,7 @@ export default getBaseRepository(SessionEntity).extend({ posts: undefined, columns: undefined, createdBy: { id: authorId }, + moderator: { id: authorId }, }; delete sessionWithoutPosts.posts; delete sessionWithoutPosts.columns; diff --git a/backend/src/db/repositories/UserRepository.ts b/backend/src/db/repositories/UserRepository.ts index 0028e9af7..595dd1232 100644 --- a/backend/src/db/repositories/UserRepository.ts +++ b/backend/src/db/repositories/UserRepository.ts @@ -19,4 +19,20 @@ export default getBaseRepository(UserEntity).extend({ } return null; }, + async getRelatedUsersIds(userId: string): Promise { + const ids: Array<{ id: string }> = await this.query( + ` + select distinct u2.id from users u + left join visitors v on v.users_id = u.id + left join sessions s on v.sessions_id = s.id + left join visitors v2 on v2.sessions_id = s.id + left join users u2 on v2.users_id = u2.id + where + u.id = $1 and + u2.email is not null + `, + [userId] + ); + return ids.map((i) => i.id); + }, }); diff --git a/backend/src/game.ts b/backend/src/game.ts index cea0db27c..c6d8d29a5 100644 --- a/backend/src/game.ts +++ b/backend/src/game.ts @@ -3,19 +3,15 @@ import { Post, PostGroup, Participant, - ColumnDefinition, UnauthorizedAccessPayload, WsUserData, - WsNameData, WsLikeUpdatePayload, WsPostUpdatePayload, WsDeletePostPayload, WsDeleteGroupPayload, - WsSaveTemplatePayload, WsReceiveLikeUpdatePayload, WsErrorType, Session, - SessionOptions, WsErrorPayload, WebsocketMessage, WsGroupUpdatePayload, @@ -24,6 +20,8 @@ import { WsCancelVotesPayload, WsReceiveCancelVotesPayload, WsReceiveTimerStartPayload, + WsSaveSessionSettingsPayload, + SessionSettings, } from './common/index.js'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import chalk from 'chalk-template'; @@ -50,6 +48,7 @@ import { doesSessionExists, wasSessionCreatedBy, toggleReady, + updateModerator, } from './db/actions/sessions.js'; import { getUser, getUserView } from './db/actions/users.js'; import { @@ -90,15 +89,9 @@ const { DELETE_POST_GROUP, EDIT_POST_GROUP, RECEIVE_CLIENT_LIST, - RECEIVE_SESSION_NAME, JOIN_SESSION, - RENAME_SESSION, LEAVE_SESSION, - EDIT_OPTIONS, - RECEIVE_OPTIONS, - EDIT_COLUMNS, - RECEIVE_COLUMNS, - SAVE_TEMPLATE, + SAVE_SESSION_SETTINGS, LOCK_SESSION, RECEIVE_LOCK_SESSION, RECEIVE_UNAUTHORIZED, @@ -111,6 +104,7 @@ const { STOP_TIMER, RECEIVE_TIMER_START, RECEIVE_TIMER_STOP, + RECEIVE_SESSION_SETTINGS, } = Actions; interface Users { @@ -126,7 +120,7 @@ const s = (str: string) => chalk`{blue ${str.replace('retrospected/', '')}}`; export default (io: Server) => { const users: Users = {}; - const d = () => chalk`{yellow [${moment().format('HH:mm:ss')}]} `; + const d = () => chalk`{yellow [${moment().format('D/M HH:mm:ss')}]} `; const getRoom = (sessionId: string) => `board-${sessionId}`; @@ -388,22 +382,6 @@ export default (io: Server) => { } }; - const onRenameSession = async ( - _userIds: UserIds | null, - sessionId: string, - data: WsNameData, - socket: Socket - ) => { - const success = await updateName(sessionId, data.name); - sendToAllOrError( - socket, - sessionId, - RECEIVE_SESSION_NAME, - 'cannot_rename_session', - success ? data.name : null - ); - }; - const onLeaveSession = async ( _userIds: UserIds | null, sessionId: string, @@ -563,49 +541,46 @@ export default (io: Server) => { } }; - const onEditOptions = async ( - _userIds: UserIds | null, + const onSaveSessionSettings = async ( + userIds: UserIds | null, sessionId: string, - data: SessionOptions, + data: WsSaveSessionSettingsPayload, socket: Socket ) => { - const options = await updateOptions(sessionId, data); - sendToAllOrError( - socket, - sessionId, - RECEIVE_OPTIONS, - 'cannot_save_options', - options - ); - }; + if (data.settings.options !== undefined) { + await updateOptions(sessionId, data.settings.options); + } - const onEditColumns = async ( - _userIds: UserIds | null, - sessionId: string, - data: ColumnDefinition[], - socket: Socket - ) => { - const columns = await updateColumns(sessionId, data); - sendToAllOrError( + if (data.settings.columns !== undefined) { + await updateColumns(sessionId, data.settings.columns); + } + + if (data.settings.name !== undefined) { + await updateName(sessionId, data.settings.name || ''); + } + + if (data.settings.moderator !== undefined) { + await updateModerator(sessionId, data.settings.moderator.id); + } + + if (checkUser(userIds, socket) && data.saveAsTemplate) { + if (data.settings.columns && data.settings.options) { + await saveTemplate( + userIds.userId, + data.settings.columns, + data.settings.options + ); + } + } + sendToAllOrError( socket, sessionId, - RECEIVE_COLUMNS, - 'cannot_save_columns', - columns + RECEIVE_SESSION_SETTINGS, + 'cannot_save_session_settings', + data.settings ); }; - const onSaveTemplate = async ( - userIds: UserIds | null, - _sessionId: string, - data: WsSaveTemplatePayload, - socket: Socket - ) => { - if (checkUser(userIds, socket)) { - await saveTemplate(userIds.userId, data.columns, data.options); - } - }; - const onLockSession = async ( _userIds: UserIds | null, sessionId: string, @@ -696,12 +671,9 @@ export default (io: Server) => { { type: JOIN_SESSION, handler: onJoinSession }, { type: REQUEST_BOARD, handler: onRequestBoard }, - { type: RENAME_SESSION, handler: onRenameSession }, { type: LEAVE_SESSION, handler: onLeaveSession }, { type: USER_READY, handler: onUserReady }, - { type: EDIT_OPTIONS, handler: onEditOptions, onlyAuthor: true }, - { type: EDIT_COLUMNS, handler: onEditColumns, onlyAuthor: true }, - { type: SAVE_TEMPLATE, handler: onSaveTemplate, onlyAuthor: true }, + { type: SAVE_SESSION_SETTINGS, handler: onSaveSessionSettings }, { type: LOCK_SESSION, handler: onLockSession, onlyAuthor: true }, ]; diff --git a/backend/src/index.ts b/backend/src/index.ts index 8ee89c31c..081122e7b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -59,6 +59,7 @@ import { getIdentityByUsername, associateUserWithAdWordsCampaign, TrackingInfo, + getRelatedUsers, } from './db/actions/users.js'; import { isLicenced } from './security/is-licenced.js'; import rateLimit from 'express-rate-limit'; @@ -565,6 +566,15 @@ db().then(() => { } }); + app.get('/api/users', heavyLoadLimiter, async (req, res) => { + const user = await getUserViewFromRequest(req); + if (user) { + const users = await getRelatedUsers(user.id); + return res.status(200).send(users); + } + return res.status(401).send('Not logged in'); + }); + app.post('/api/validate', heavyLoadLimiter, async (req, res) => { const validatePayload = req.body as ValidateEmailPayload; const identity = await getPasswordIdentity(validatePayload.email); diff --git a/frontend/src/common/actions.ts b/frontend/src/common/actions.ts index bcf7de433..470c557a2 100644 --- a/frontend/src/common/actions.ts +++ b/frontend/src/common/actions.ts @@ -9,12 +9,9 @@ const actions = { ADD_POST_GROUP_SUCCESS: 'retrospected/group/add/success', DELETE_POST_GROUP: 'retrospected/group/delete', EDIT_POST_GROUP: 'retrospected/group/edit', - RENAME_SESSION: 'retrospected/session/rename', JOIN_SESSION: 'retrospected/session/join', LEAVE_SESSION: 'retrospected/session/leave', - EDIT_OPTIONS: 'retrospected/session/options/edit', - EDIT_COLUMNS: 'retrospected/session/columns/edit', - SAVE_TEMPLATE: 'retrospected/session/template/save', + SAVE_SESSION_SETTINGS: 'retrospected/session/settings/save', LOCK_SESSION: 'retrospected/session/lock', REQUEST_BOARD: 'retrospected/session/request', USER_READY: 'retrospected/user-ready', @@ -32,10 +29,8 @@ const actions = { RECEIVE_DELETE_POST_GROUP: 'retrospected/group/receive/delete', RECEIVE_EDIT_POST_GROUP: 'retrospected/group/receive/edit', RECEIVE_BOARD: 'retrospected/posts/receive-all', - RECEIVE_OPTIONS: 'retrospected/session/options/receive', - RECEIVE_COLUMNS: 'retrospected/session/columns/receive', + RECEIVE_SESSION_SETTINGS: 'retrospected/session/settings/receive', RECEIVE_CLIENT_LIST: 'retrospected/session/receive/client-list', - RECEIVE_SESSION_NAME: 'retrospected/session/receive/rename', RECEIVE_LOCK_SESSION: 'retrospected/session/receive/lock', RECEIVE_UNAUTHORIZED: 'retrospected/session/receive/unauthorized', RECEIVE_RATE_LIMITED: 'retrospected/rate-limited-error', diff --git a/frontend/src/common/models.ts b/frontend/src/common/models.ts index 9d9d99bb0..06c0f5bcd 100644 --- a/frontend/src/common/models.ts +++ b/frontend/src/common/models.ts @@ -17,12 +17,12 @@ export const defaultOptions: SessionOptions = { allowTimer: true, timerDuration: 15 * 60, readonlyOnTimerEnd: true, - restrictTitleEditToOwner: false, - restrictReorderingToOwner: false, - restrictGroupingToOwner: false, + restrictTitleEditToModerator: false, + restrictReorderingToModerator: false, + restrictGroupingToModerator: false, }; -export const defaultSession: Omit = { +export const defaultSession: Omit = { id: '', columns: [ { id: '', index: 0, label: '', type: 'well', color: '', icon: null }, diff --git a/frontend/src/common/types.ts b/frontend/src/common/types.ts index 768e4449c..bc4a8d493 100644 --- a/frontend/src/common/types.ts +++ b/frontend/src/common/types.ts @@ -1,15 +1,21 @@ -export interface Session extends PostContainer, Entity { +export type SessionSettings = Partial; + +export interface AllSessionSettings { name: string | null; + moderator: User; + options: SessionOptions; + columns: ColumnDefinition[]; + locked: boolean; + timer: Date | null; +} + +export interface Session extends AllSessionSettings, PostContainer, Entity { posts: Post[]; groups: PostGroup[]; - columns: ColumnDefinition[]; messages: Message[]; - options: SessionOptions; encrypted: string | null; - locked: boolean; createdBy: User; ready: string[]; - timer: Date | null; demo: boolean; } @@ -60,9 +66,9 @@ export interface SessionOptions { allowGrouping: boolean; allowReordering: boolean; allowCancelVote: boolean; - restrictTitleEditToOwner: boolean; - restrictReorderingToOwner: boolean; - restrictGroupingToOwner: boolean; + restrictTitleEditToModerator: boolean; + restrictReorderingToModerator: boolean; + restrictGroupingToModerator: boolean; blurCards: boolean; newPostsFirst: boolean; allowTimer: boolean; @@ -189,11 +195,11 @@ export type AdminStats = { clients: number; }; -export type CoachRole = 'user' | 'assistant' | 'system'; +export type CoachRole = 'user' | 'assistant' | 'system' | 'function'; export type CoachMessage = { role: CoachRole; - content: string; + content?: string; }; export type TrackingEvent = @@ -211,6 +217,7 @@ export type TrackingEvent = | 'home/load-previous' | 'game/session/edit-options' | 'game/session/edit-columns' + | 'game/session/save-options' | 'game/session/reset' | 'game/session/disconnect' | 'game/session/unexpected-disconnection' diff --git a/frontend/src/common/ws.ts b/frontend/src/common/ws.ts index f879e36cd..a559f21c3 100644 --- a/frontend/src/common/ws.ts +++ b/frontend/src/common/ws.ts @@ -1,8 +1,7 @@ import { - ColumnDefinition, Post, PostGroup, - SessionOptions, + SessionSettings, User, VoteExtract, VoteType, @@ -18,10 +17,6 @@ export interface WsUserData { user: User; } -export interface WsNameData { - name: string; -} - export interface WsPostUpdatePayload { post: Omit, 'user'>, 'group'>; groupId: string | null; @@ -57,10 +52,9 @@ export interface WsCancelVotesPayload { export interface WsDeleteGroupPayload { groupId: string; } - -export interface WsSaveTemplatePayload { - columns: ColumnDefinition[]; - options: SessionOptions; +export interface WsSaveSessionSettingsPayload { + settings: SessionSettings; + saveAsTemplate: boolean; } export interface WsUserReadyPayload { diff --git a/frontend/src/testing/index.tsx b/frontend/src/testing/index.tsx index 9ac31a3d9..d6636e6bb 100644 --- a/frontend/src/testing/index.tsx +++ b/frontend/src/testing/index.tsx @@ -51,6 +51,12 @@ export const initialSession: Session = { photo: null, email: null, }, + moderator: { + id: 'John Doe', + name: 'John Doe', + photo: null, + email: null, + }, options: { ...defaultOptions, }, diff --git a/frontend/src/translations/locales/ar-SA.json b/frontend/src/translations/locales/ar-SA.json index 2dd041bb3..b4c67a81e 100644 --- a/frontend/src/translations/locales/ar-SA.json +++ b/frontend/src/translations/locales/ar-SA.json @@ -72,6 +72,8 @@ "title": "تخصيص الجلسة الخاصة بك", "timerCategory": "المؤقت", "timerCategorySub": "تعيين المؤقت للدورة", + "boardCategory": "المجلس", + "boardCategorySub": "تخصيص إعدادات اللوحة", "allowTimer": "السماح بالتوقيت", "allowTimerHelp": "عرض المؤقت في الجزء السفلي من الشاشة", "timerDuration": "مدة المؤقت", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "قفل الجلسة (قم بقراءتها فقط) عند انتهاء المؤقت", "votingCategory": "التصويت", "votingCategorySub": "تعيين القواعد حول الإعجاب و عدم الإعجاب", - "postCategory": "إعدادات النشر", + "postCategory": "المشاركات", "postCategorySub": "تعيين القواعد حول ما يمكن للمستخدم أن يفعله عند إنشاء أو عرض مشاركة", "customTemplateCategory": "قالب العمود", "customTemplateCategorySub": "حدد قالب وقم بتخصيص الأعمدة الخاصة بك", @@ -123,7 +125,11 @@ "templateHelp": "استخدام مجموعة محددة مسبقاً من الأعمدة", "numberOfColumns": "عدد الأعمدة", "numberOfColumnsHelp": "تعيين عدد الأعمدة", - "makeDefaultTemplate": "اجعل هذا قالبي الافتراضي" + "makeDefaultTemplate": "اجعل هذا قالبي الافتراضي", + "changeModerator": "تغيير المشرف", + "changeModeratorHelp": "تغيير مشرف هذه الجلسة. يمكن للمشرف تعديل إعدادات الجلسة", + "owner": "المالك", + "ownerHelp": "مالك الجلسة هو الشخص الذي أنشأها. هذا لا يمكن تغييره." }, "PostBoard": { "customQuestion": "عمود مخصص", diff --git a/frontend/src/translations/locales/de-DE.json b/frontend/src/translations/locales/de-DE.json index 3f7cb20a6..fab5b2cf9 100644 --- a/frontend/src/translations/locales/de-DE.json +++ b/frontend/src/translations/locales/de-DE.json @@ -72,6 +72,8 @@ "title": "Personalisieren Sie die Sitzung", "timerCategory": "Timer", "timerCategorySub": "Timer für die Sitzung festlegen", + "boardCategory": "Board", + "boardCategorySub": "Board-Einstellungen anpassen", "allowTimer": "Timer erlauben", "allowTimerHelp": "Timer am unteren Bildschirmrand anzeigen", "timerDuration": "Timer-Dauer", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Sitzung sperren (nur lesen) wenn der Timer endet", "votingCategory": "Abstimmung", "votingCategorySub": "Setze die Abstimmregeln", - "postCategory": "Beitragseinstellungen", + "postCategory": "Beiträge", "postCategorySub": "Stelle ein, wie Nutzer mit Beiträgen interagieren können", "customTemplateCategory": "Spaltenkonfiguration", "customTemplateCategorySub": "Wählen Sie eine Vorlage aus und passen Sie Ihre Spalten an", @@ -123,7 +125,11 @@ "templateHelp": "Nutze ein vordefiniertes Spaltenset", "numberOfColumns": "Anzahl an Spalten", "numberOfColumnsHelp": "Setze die Anzahl an Spalten", - "makeDefaultTemplate": "Diese Standardvorlage erstellen" + "makeDefaultTemplate": "Diese Standardvorlage erstellen", + "changeModerator": "Moderator ändern", + "changeModeratorHelp": "Moderator dieser Sitzung ändern. Der Moderator kann die Sitzungseinstellungen bearbeiten", + "owner": "Besitzer", + "ownerHelp": "Der Besitzer der Sitzung ist die Person, die sie erstellt hat. Dies kann nicht geändert werden." }, "PostBoard": { "customQuestion": "Eigene Spalte", @@ -230,13 +236,7 @@ "nameField": "Ihr Name (für Anzeigezwecke)", "noAuthWarning": "Ihr Administrator hat alle Login-Möglichkeiten (OAuth, Passwort) deaktiviert. Bitten Sie Ihren Administrator, mindestens eine wieder zu aktivieren.", "or": "oder", - "passwordScoreWords": [ - "schwach", - "schwach", - "nicht ganz", - "gut", - "stark" - ], + "passwordScoreWords": ["schwach", "schwach", "nicht ganz", "gut", "stark"], "skipAndAnonLogin": "Anonym verwenden" }, "AccountLogin": { diff --git a/frontend/src/translations/locales/en-GB.json b/frontend/src/translations/locales/en-GB.json index 9dcfa4eb1..24c171b0b 100644 --- a/frontend/src/translations/locales/en-GB.json +++ b/frontend/src/translations/locales/en-GB.json @@ -72,6 +72,8 @@ "title": "Customise your Session", "timerCategory": "Timer", "timerCategorySub": "Set the timer for the session", + "boardCategory": "Board", + "boardCategorySub": "Customise board settings", "allowTimer": "Allow Timer", "allowTimerHelp": "Display a timer at the bottom of the screen", "timerDuration": "Timer Duration", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Lock the session (make it read-only) when the timer ends", "votingCategory": "Voting", "votingCategorySub": "Set the rules about likes and dislikes", - "postCategory": "Post settings", + "postCategory": "Posts", "postCategorySub": "Set the rules about what a user can do when creating or viewing a post", "customTemplateCategory": "Column Template", "customTemplateCategorySub": "Select a template and customize your columns", @@ -123,7 +125,11 @@ "templateHelp": "Use a pre-defined set of columns", "numberOfColumns": "Number of columns", "numberOfColumnsHelp": "Set the number of columns", - "makeDefaultTemplate": "Make this my default template" + "makeDefaultTemplate": "Make this my default template", + "changeModerator": "Change the Moderator", + "changeModeratorHelp": "Change the moderator of this session. The moderator can also edit the session settings.", + "owner": "Owner", + "ownerHelp": "The owner of the session is the person who created it. They are also a moderator. This cannot be changed." }, "PostBoard": { "customQuestion": "Custom Column", diff --git a/frontend/src/translations/locales/es-ES.json b/frontend/src/translations/locales/es-ES.json index 63b6d3272..61a52be46 100644 --- a/frontend/src/translations/locales/es-ES.json +++ b/frontend/src/translations/locales/es-ES.json @@ -72,6 +72,8 @@ "title": "Personaliza tu sesión", "timerCategory": "Temporizador", "timerCategorySub": "Establecer el temporizador de la sesión", + "boardCategory": "Tablero", + "boardCategorySub": "Personalizar ajustes del tablero", "allowTimer": "Permitir temporizador", "allowTimerHelp": "Mostrar un temporizador en la parte inferior de la pantalla", "timerDuration": "Duración del temporizador", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Bloquear la sesión (hacer sólo lectura) cuando finalice el temporizador", "votingCategory": "Votaciones", "votingCategorySub": "Establecer las reglas sobre \"me gusta\" y \"me gusta\"", - "postCategory": "Ajustes de publicación", + "postCategory": "Mensajes", "postCategorySub": "Establecer las reglas sobre lo que un usuario puede hacer al crear o ver un mensaje", "customTemplateCategory": "Plantilla de columna", "customTemplateCategorySub": "Seleccione una plantilla y personalice sus columnas", @@ -123,7 +125,11 @@ "templateHelp": "Usar un conjunto predefinido de columnas", "numberOfColumns": "Número de columnas", "numberOfColumnsHelp": "Establecer el número de columnas", - "makeDefaultTemplate": "Hacer esta mi plantilla predeterminada" + "makeDefaultTemplate": "Hacer esta mi plantilla predeterminada", + "changeModerator": "Cambiar el moderador", + "changeModeratorHelp": "Cambiar el moderador de esta sesión. El moderador puede editar la configuración de la sesión", + "owner": "Propietario", + "ownerHelp": "El propietario de la sesión es la persona que la creó. Esto no se puede cambiar." }, "PostBoard": { "customQuestion": "Columna personalizada", diff --git a/frontend/src/translations/locales/fr-FR.json b/frontend/src/translations/locales/fr-FR.json index f0c03a26a..8144ce249 100644 --- a/frontend/src/translations/locales/fr-FR.json +++ b/frontend/src/translations/locales/fr-FR.json @@ -72,6 +72,8 @@ "title": "Nouvelle session personalisée", "timerCategory": "Minuteur", "timerCategorySub": "Régler le minuteur pour la session", + "boardCategory": "Board", + "boardCategorySub": "Personnalisez les paramètres de la rétro", "allowTimer": "Utiliser le minuteur", "allowTimerHelp": "Afficher un minuteur en bas de l'écran", "timerDuration": "Durée du minuteur", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Verrouiller la session (lecture seule) quand le minuteur se termine", "votingCategory": "Votes", "votingCategorySub": "Règles concernant les votes", - "postCategory": "Options des posts", + "postCategory": "Posts", "postCategorySub": "Règles concernant ce qu'un utilisateur peut faire sur un post", "customTemplateCategory": "Colonnes personalisées", "customTemplateCategorySub": "Sélectionnez un modèle et personnalisez vos colonnes", @@ -123,7 +125,11 @@ "templateHelp": "Sélectionnez un jeu de colonnes prédéfini", "numberOfColumns": "Nombre de colonnes", "numberOfColumnsHelp": "Réglez le nombre de colonnes", - "makeDefaultTemplate": "En faire mes réglages par défaut" + "makeDefaultTemplate": "En faire mes réglages par défaut", + "changeModerator": "Changer le modérateur", + "changeModeratorHelp": "Changer le modérateur de cette session. Le modérateur peut modifier les paramètres de la session.", + "owner": "Propriétaire", + "ownerHelp": "Le propriétaire de la session est la personne qui l'a créée. Cela ne peut être changé." }, "PostBoard": { "customQuestion": "Colonne personnalisée", diff --git a/frontend/src/translations/locales/hu-HU.json b/frontend/src/translations/locales/hu-HU.json index e09c5b767..36e6173e1 100644 --- a/frontend/src/translations/locales/hu-HU.json +++ b/frontend/src/translations/locales/hu-HU.json @@ -72,6 +72,8 @@ "title": "A munkamenet testreszabása", "timerCategory": "", "timerCategorySub": "", + "boardCategory": "", + "boardCategorySub": "", "allowTimer": "", "allowTimerHelp": "", "timerDuration": "", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "", "votingCategory": "Szavazás", "votingCategorySub": "Állítsd be a tetszésnyilvánításokra és a nemtetszésekre vonatkozó szabályokat", - "postCategory": "Hozzászólás beállításai", + "postCategory": "", "postCategorySub": "Állítsa be a szabályokat arra vonatkozóan, hogy a felhasználó mit tehet bejegyzés létrehozásakor vagy megtekintésekor", "customTemplateCategory": "Oszlop sablon", "customTemplateCategorySub": "", @@ -123,7 +125,11 @@ "templateHelp": "Használjon előre meghatározott oszlopkészletet", "numberOfColumns": "Oszlopok száma", "numberOfColumnsHelp": "Állítsa be az oszlopok számát", - "makeDefaultTemplate": "Legyen ez az alapértelmezett sablonom" + "makeDefaultTemplate": "Legyen ez az alapértelmezett sablonom", + "changeModerator": "", + "changeModeratorHelp": "", + "owner": "", + "ownerHelp": "" }, "PostBoard": { "customQuestion": "Egyéni oszlop", diff --git a/frontend/src/translations/locales/it-IT.json b/frontend/src/translations/locales/it-IT.json index 7a54c3a33..81c714ef0 100644 --- a/frontend/src/translations/locales/it-IT.json +++ b/frontend/src/translations/locales/it-IT.json @@ -72,6 +72,8 @@ "title": "Personalizza il tuo gioco!", "timerCategory": "Timer", "timerCategorySub": "Imposta il timer per la sessione", + "boardCategory": "Tavola", + "boardCategorySub": "Personalizza le impostazioni della scheda", "allowTimer": "Consenti Timer", "allowTimerHelp": "Mostra un timer nella parte inferiore dello schermo", "timerDuration": "Durata Timer", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Blocca la sessione (fai in sola lettura) quando il timer finisce", "votingCategory": "Votazione", "votingCategorySub": "Imposta tutte le regole relative a \"mi piace\" e \"non mi piace\"", - "postCategory": "Impostazioni del Post", + "postCategory": "Post", "postCategorySub": "Imposta le azioni che l'utente può fare quando crea o vede un post ", "customTemplateCategory": "Template di Colonna", "customTemplateCategorySub": "Seleziona un modello e personalizza le tue colonne", @@ -123,7 +125,11 @@ "templateHelp": "Usa un set di colonne predefinito", "numberOfColumns": "Numbero di colonne", "numberOfColumnsHelp": "Imposta il numero di colonne", - "makeDefaultTemplate": "Rendi questo template quello di default" + "makeDefaultTemplate": "Rendi questo template quello di default", + "changeModerator": "Cambia il moderatore", + "changeModeratorHelp": "Cambia il moderatore di questa sessione. Il moderatore può modificare le impostazioni della sessione", + "owner": "Proprietario", + "ownerHelp": "Il proprietario della sessione è la persona che l'ha creata. Questo non può essere modificato." }, "PostBoard": { "customQuestion": "Colonna personalizzata", diff --git a/frontend/src/translations/locales/ja-JP.json b/frontend/src/translations/locales/ja-JP.json index b0d639118..80b0a18bc 100644 --- a/frontend/src/translations/locales/ja-JP.json +++ b/frontend/src/translations/locales/ja-JP.json @@ -72,6 +72,8 @@ "title": "セッションをカスタマイズ", "timerCategory": "タイマー", "timerCategorySub": "セッションのタイマーを設定する", + "boardCategory": "ボード", + "boardCategorySub": "ボードの設定をカスタマイズ", "allowTimer": "タイマーを許可", "allowTimerHelp": "画面下部にタイマーを表示する", "timerDuration": "タイマー時間", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "タイマーが終了したときにセッションをロックする", "votingCategory": "投票", "votingCategorySub": "「いいね」と「嫌い」に関するルールを設定", - "postCategory": "投稿設定", + "postCategory": "投稿", "postCategorySub": "投稿を作成または表示する際にユーザができることに関するルールを設定します", "customTemplateCategory": "列テンプレート", "customTemplateCategorySub": "テンプレートを選択し、列をカスタマイズします", @@ -123,7 +125,11 @@ "templateHelp": "あらかじめ定義された列のセットを使用", "numberOfColumns": "列数", "numberOfColumnsHelp": "列数を設定", - "makeDefaultTemplate": "これをデフォルトのテンプレートにする" + "makeDefaultTemplate": "これをデフォルトのテンプレートにする", + "changeModerator": "モデレーターの変更", + "changeModeratorHelp": "このセッションのモデレータを変更します。モデレータはセッションの設定を編集できます。", + "owner": "所有者", + "ownerHelp": "セッションの所有者はそれを作成した人です。これは変更できません。" }, "PostBoard": { "customQuestion": "カスタム列", diff --git a/frontend/src/translations/locales/nl-NL.json b/frontend/src/translations/locales/nl-NL.json index 41f731570..a9bee2c3a 100644 --- a/frontend/src/translations/locales/nl-NL.json +++ b/frontend/src/translations/locales/nl-NL.json @@ -72,6 +72,8 @@ "title": "Sessie aanpassen", "timerCategory": "Timer", "timerCategorySub": "Zet de timer voor de sessie", + "boardCategory": "Bord", + "boardCategorySub": "Aanpassen board instellingen", "allowTimer": "Timer toestaan", "allowTimerHelp": "Toon een timer aan de onderkant van het scherm", "timerDuration": "Timer duur", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Vergrendel de sessie (maak het alleen-lezen) wanneer de timer eindigt", "votingCategory": "Stemmen", "votingCategorySub": "Instellen regels voor stemmen", - "postCategory": "Retropunten instellingen", + "postCategory": "Berichten", "postCategorySub": "Instellen regels wat een gebruiker kan doen wanneer hij reageert op een retropunt", "customTemplateCategory": "Kolommen template", "customTemplateCategorySub": "Selecteer een sjabloon en pas uw kolommen aan", @@ -123,7 +125,11 @@ "templateHelp": "Gebruik een vooringestelde set kolommen", "numberOfColumns": "Aantal kolommen", "numberOfColumnsHelp": "Stel het aantal kolommen in", - "makeDefaultTemplate": "Stel in als mijn standaard template" + "makeDefaultTemplate": "Stel in als mijn standaard template", + "changeModerator": "Wijzig de moderator", + "changeModeratorHelp": "De moderator van deze sessie wijzigen. De moderator kan de sessie-instellingen bewerken", + "owner": "Eigenaar", + "ownerHelp": "De eigenaar van de sessie is de persoon die de sessie heeft gemaakt. Dit kan niet worden gewijzigd." }, "PostBoard": { "customQuestion": "Aangepast kolom", diff --git a/frontend/src/translations/locales/pl-PL.json b/frontend/src/translations/locales/pl-PL.json index 57793fc3c..f52d00a93 100644 --- a/frontend/src/translations/locales/pl-PL.json +++ b/frontend/src/translations/locales/pl-PL.json @@ -72,6 +72,8 @@ "title": "Dostosuj swoją sesję", "timerCategory": "Timer", "timerCategorySub": "Ustaw czasomierz dla sesji", + "boardCategory": "Tablica", + "boardCategorySub": "Dostosuj ustawienia tablicy", "allowTimer": "Zezwalaj na Timer", "allowTimerHelp": "Wyświetlaj licznik na dole ekranu", "timerDuration": "Czas trwania zegara", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Zablokuj sesję (utwórz tylko do odczytu) po zakończeniu timera", "votingCategory": "Głosowanie", "votingCategorySub": "Ustaw reguły dotyczące polubień i polubień", - "postCategory": "Ustawienia postów", + "postCategory": "Posty", "postCategorySub": "Ustaw reguły co użytkownik może zrobić podczas tworzenia lub przeglądania wpisu", "customTemplateCategory": "Szablon kolumny", "customTemplateCategorySub": "Wybierz szablon i dostosuj swoje kolumny", @@ -123,7 +125,11 @@ "templateHelp": "Użyj predefiniowanego zestawu kolumn", "numberOfColumns": "Liczba kolumn", "numberOfColumnsHelp": "Ustaw liczbę kolumn", - "makeDefaultTemplate": "Ustaw ten domyślny szablon" + "makeDefaultTemplate": "Ustaw ten domyślny szablon", + "changeModerator": "Zmień moderatora", + "changeModeratorHelp": "Zmień moderatora tej sesji. Moderator może edytować ustawienia sesji", + "owner": "Właściciel", + "ownerHelp": "Właścicielem sesji jest osoba, która ją stworzyła. Nie można tego zmienić." }, "PostBoard": { "customQuestion": "Kolumna niestandardowa", diff --git a/frontend/src/translations/locales/pt-BR.json b/frontend/src/translations/locales/pt-BR.json index e3b7aad27..035faf2ce 100644 --- a/frontend/src/translations/locales/pt-BR.json +++ b/frontend/src/translations/locales/pt-BR.json @@ -72,6 +72,8 @@ "title": "Personalize sua sessão", "timerCategory": "Cronômetro", "timerCategorySub": "Definir o temporizador para a sessão", + "boardCategory": "Tabuleiro", + "boardCategorySub": "Personalizar configurações do board", "allowTimer": "Permitir Temporizador", "allowTimerHelp": "Exibir um cronômetro na parte inferior da tela", "timerDuration": "Duração do temporizador", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Bloquear a sessão (torná-la somente leitura) quando o timer terminar", "votingCategory": "Votação", "votingCategorySub": "Definir regras sobre curtidas e não curtidas", - "postCategory": "Configurações de postagem", + "postCategory": "Postagens", "postCategorySub": "Defina as regras sobre o que um usuário pode fazer ao criar ou visualizar uma postagem", "customTemplateCategory": "Modelo de coluna", "customTemplateCategorySub": "Selecione um modelo e personalize suas colunas", @@ -123,7 +125,11 @@ "templateHelp": "Usar um conjunto de colunas pré-definidas", "numberOfColumns": "Número de colunas", "numberOfColumnsHelp": "Definir o número de colunas", - "makeDefaultTemplate": "Tornar este meu modelo padrão" + "makeDefaultTemplate": "Tornar este meu modelo padrão", + "changeModerator": "Alterar o Moderador", + "changeModeratorHelp": "Alterar o moderador desta sessão. O moderador pode editar as configurações da sessão", + "owner": "Proprietário", + "ownerHelp": "O proprietário da sessão é a pessoa que a criou. Isto não pode ser alterado." }, "PostBoard": { "customQuestion": "Coluna personalizada", diff --git a/frontend/src/translations/locales/pt-PT.json b/frontend/src/translations/locales/pt-PT.json index e3f0b3986..f786c2d18 100644 --- a/frontend/src/translations/locales/pt-PT.json +++ b/frontend/src/translations/locales/pt-PT.json @@ -72,6 +72,8 @@ "title": "Personalize sua sessão", "timerCategory": "Cronômetro", "timerCategorySub": "Definir o temporizador para a sessão", + "boardCategory": "Tabuleiro", + "boardCategorySub": "Personalizar configurações do board", "allowTimer": "Permitir Temporizador", "allowTimerHelp": "Exibir um cronômetro na parte inferior da tela", "timerDuration": "Duração do temporizador", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Bloquear a sessão (torná-la somente leitura) quando o timer terminar", "votingCategory": "Votação", "votingCategorySub": "Definir regras sobre curtidas e não curtidas", - "postCategory": "Configurações de postagem", + "postCategory": "Postagens", "postCategorySub": "Defina as regras sobre o que um usuário pode fazer ao criar ou visualizar uma postagem", "customTemplateCategory": "Modelo de coluna", "customTemplateCategorySub": "Selecione um modelo e personalize suas colunas", @@ -123,7 +125,11 @@ "templateHelp": "Usar um conjunto de colunas pré-definidas", "numberOfColumns": "Número de colunas", "numberOfColumnsHelp": "Definir o número de colunas", - "makeDefaultTemplate": "Tornar este meu modelo padrão" + "makeDefaultTemplate": "Tornar este meu modelo padrão", + "changeModerator": "Alterar o Moderador", + "changeModeratorHelp": "Alterar o moderador desta sessão. O moderador pode editar as configurações da sessão", + "owner": "Proprietário", + "ownerHelp": "O proprietário da sessão é a pessoa que a criou. Isto não pode ser alterado." }, "PostBoard": { "customQuestion": "Coluna personalizada", diff --git a/frontend/src/translations/locales/uk-UA.json b/frontend/src/translations/locales/uk-UA.json index c4ece2a0f..b82c19784 100644 --- a/frontend/src/translations/locales/uk-UA.json +++ b/frontend/src/translations/locales/uk-UA.json @@ -72,6 +72,8 @@ "title": "Налаштуйте вашу сесію", "timerCategory": "Таймер", "timerCategorySub": "Встановити таймер для сеансу", + "boardCategory": "Дошка", + "boardCategorySub": "Налаштування параметрів дошки", "allowTimer": "Дозволити таймер", "allowTimerHelp": "Відображати таймер внизу екрана", "timerDuration": "Тривалість таймера", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "Блокувати сесію (зробити її тільки для читання) коли таймер завершується", "votingCategory": "Голосування", "votingCategorySub": "Встановіть правила про подібні та вподобання", - "postCategory": "Налаштування повідомлень", + "postCategory": "Повідомлення", "postCategorySub": "Встановіть правила щодо того, що може робити користувач при створенні чи перегляді допису", "customTemplateCategory": "Шаблон стовпця", "customTemplateCategorySub": "Виберіть шаблон і налаштуйте стовпці", @@ -123,7 +125,11 @@ "templateHelp": "Використовувати попередньо визначений набір стовпців", "numberOfColumns": "Кількість колонок", "numberOfColumnsHelp": "Встановити кількість стовпців", - "makeDefaultTemplate": "Зробити це моїм типовим шаблоном" + "makeDefaultTemplate": "Зробити це моїм типовим шаблоном", + "changeModerator": "Змінити модератора", + "changeModeratorHelp": "Змінити модератора цього сеансу модератор може редагувати налаштування сесії", + "owner": "Власник", + "ownerHelp": "Власником сесії є людина, яка його створила. Це не можна змінити." }, "PostBoard": { "customQuestion": "Власний стовпець", diff --git a/frontend/src/translations/locales/zh-CN.json b/frontend/src/translations/locales/zh-CN.json index 547c52db0..da2fbd05b 100644 --- a/frontend/src/translations/locales/zh-CN.json +++ b/frontend/src/translations/locales/zh-CN.json @@ -72,6 +72,8 @@ "title": "自定义您的会话", "timerCategory": "定时器", "timerCategorySub": "设置会话计时器", + "boardCategory": "棋盘", + "boardCategorySub": "自定义棋盘设置", "allowTimer": "允许计时器", "allowTimerHelp": "在屏幕底部显示计时器", "timerDuration": "计时器持续时间", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "当计时器结束时锁定会话(只读)", "votingCategory": "表 决", "votingCategorySub": "设置关于喜欢和不喜欢的规则", - "postCategory": "帖子设置", + "postCategory": "员额", "postCategorySub": "设置用户在创建或查看帖子时可以做什么的规则", "customTemplateCategory": "列模板", "customTemplateCategorySub": "选择模板并自定义您的列", @@ -123,7 +125,11 @@ "templateHelp": "使用预定义的列集", "numberOfColumns": "列数", "numberOfColumnsHelp": "设置列数", - "makeDefaultTemplate": "将此设置为我的默认模板" + "makeDefaultTemplate": "将此设置为我的默认模板", + "changeModerator": "更改版主", + "changeModeratorHelp": "更改此会话的主持人。主持人可以编辑会话设置", + "owner": "所有者", + "ownerHelp": "会话的所有者是创建者。无法更改。" }, "PostBoard": { "customQuestion": "自定义列", diff --git a/frontend/src/translations/locales/zh-TW.json b/frontend/src/translations/locales/zh-TW.json index aeb549abd..cd7a34280 100644 --- a/frontend/src/translations/locales/zh-TW.json +++ b/frontend/src/translations/locales/zh-TW.json @@ -72,6 +72,8 @@ "title": "自定義您的會話", "timerCategory": "", "timerCategorySub": "", + "boardCategory": "", + "boardCategorySub": "", "allowTimer": "", "allowTimerHelp": "", "timerDuration": "", @@ -80,7 +82,7 @@ "lockOnTimerEndHelp": "", "votingCategory": "表決", "votingCategorySub": "設定好惡規則", - "postCategory": "帖子設置", + "postCategory": "", "postCategorySub": "設置用戶在創建或查看帖子時可以做什麼的規則", "customTemplateCategory": "列模板", "customTemplateCategorySub": "", @@ -123,7 +125,11 @@ "templateHelp": "使用一組預定義的列", "numberOfColumns": "列數", "numberOfColumnsHelp": "設置列數", - "makeDefaultTemplate": "將此作為我的默認模板" + "makeDefaultTemplate": "將此作為我的默認模板", + "changeModerator": "", + "changeModeratorHelp": "", + "owner": "", + "ownerHelp": "" }, "PostBoard": { "customQuestion": "自定義列", diff --git a/frontend/src/views/Game.tsx b/frontend/src/views/Game.tsx index 56a0b926e..cd9cff8f6 100644 --- a/frontend/src/views/Game.tsx +++ b/frontend/src/views/Game.tsx @@ -67,10 +67,7 @@ function GamePage() { onEditPostGroup, onLike, onCancelVotes, - onRenameSession, - onEditOptions, - onEditColumns, - onSaveTemplate, + onChangeSession, onLockSession, onUserReady, onTimerStart, @@ -198,11 +195,8 @@ function GamePage() { onCancelVotes={onCancelVotes} onDeleteGroup={onDeletePostGroup} onEditGroup={onEditPostGroup} - onRenameSession={onRenameSession} - onEditOptions={onEditOptions} - onEditColumns={onEditColumns} - onSaveTemplate={onSaveTemplate} onLockSession={onLockSession} + onChangeSession={onChangeSession} /> } /> @@ -223,7 +217,7 @@ function GamePage() { onTimerReset={onTimerReset} onTimerStart={onTimerStart} onMessage={onChatMessage} - onConfigure={onEditOptions} + onConfigure={(options) => onChangeSession({ options }, false)} /> diff --git a/frontend/src/views/game/__tests__/useSession.test.tsx b/frontend/src/views/game/__tests__/useSession.test.tsx index d8ce4c9f6..bec8efa2a 100644 --- a/frontend/src/views/game/__tests__/useSession.test.tsx +++ b/frontend/src/views/game/__tests__/useSession.test.tsx @@ -41,13 +41,13 @@ describe('useSession', () => { const context = render(); expect(context.current.session?.name).toBe('My Retro'); act(() => { - context.current.renameSession('Something else'); + context.current.editSessionSettings({ name: 'Something else' }); }); expect(context.current.session?.name).toBe('Something else'); }); }); - describe('Reseting a session', () => { + describe('Resetting a session', () => { it('Should set the session to null', () => { const context = render(); expect(context.current.session).not.toBeNull(); diff --git a/frontend/src/views/game/board/Board.tsx b/frontend/src/views/game/board/Board.tsx index 7cc4a1777..c856c31c0 100644 --- a/frontend/src/views/game/board/Board.tsx +++ b/frontend/src/views/game/board/Board.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import styled from '@emotion/styled'; -import { Post, PostGroup, SessionOptions, ColumnDefinition } from 'common'; +import { Post, PostGroup, SessionOptions, SessionSettings } from 'common'; import { DragDropContext, DropResult, @@ -26,7 +26,6 @@ interface GameModeProps { options: SessionOptions; search: string; demo: boolean; - onRenameSession: (name: string) => void; onAddPost: (columnIndex: number, content: string, rank: string) => void; onAddGroup: (columnIndex: number, rank: string) => void; onMovePost: ( @@ -42,12 +41,7 @@ interface GameModeProps { onEdit: (post: Post) => void; onEditGroup: (group: PostGroup) => void; onDeleteGroup: (group: PostGroup) => void; - onEditOptions: (options: SessionOptions) => void; - onEditColumns: (columns: ColumnDefinition[]) => void; - onSaveTemplate: ( - options: SessionOptions, - columns: ColumnDefinition[] - ) => void; + onChangeSession: (session: SessionSettings, saveAsTemplate: boolean) => void; onLockSession: (locked: boolean) => void; } @@ -73,7 +67,6 @@ const calculateRankForNewGroup = (column: ColumnContent): string => { }; function GameMode({ - onRenameSession, onAddPost, onAddGroup, onMovePost, @@ -84,9 +77,7 @@ function GameMode({ onEdit, onEditGroup, onDeleteGroup, - onEditOptions, - onEditColumns, - onSaveTemplate, + onChangeSession, onLockSession, columns, options, @@ -140,11 +131,8 @@ function GameMode({ ) : null} @@ -187,7 +175,8 @@ const Columns = styled.div<{ numberOfColumns: number }>` display: flex; margin-top: 30px; - @media screen and (max-width: ${(props) => props.numberOfColumns * 340 + 100}px) { + ${(props) => ` + @media screen and (max-width: ${props.numberOfColumns * 340 + 100}px) { margin-top: 10px; flex-direction: column; @@ -195,6 +184,7 @@ const Columns = styled.div<{ numberOfColumns: number }>` margin-bottom: 20px; } } + `} `; export default GameMode; diff --git a/frontend/src/views/game/board/__tests__/permissions-logic.test.ts b/frontend/src/views/game/board/__tests__/permissions-logic.test.ts index 7fe8eeb77..35d174c50 100644 --- a/frontend/src/views/game/board/__tests__/permissions-logic.test.ts +++ b/frontend/src/views/game/board/__tests__/permissions-logic.test.ts @@ -93,6 +93,7 @@ const session = (options: SessionOptions, ...posts: Post[]): Session => ({ posts, columns: [], createdBy: currentUser, + moderator: currentUser, options: { ...options, }, @@ -184,7 +185,7 @@ describe('Session Permission Logic', () => { const s = session({ ...defaultOptions, allowGrouping: true, - restrictGroupingToOwner: true, + restrictGroupingToModerator: true, }); const result = sessionPermissionLogic(s, currentUser, true, false); expect(result.canCreateGroup).toBe(true); @@ -195,7 +196,7 @@ describe('Session Permission Logic', () => { it('When restricting editing the title to owner', () => { const s = session({ ...defaultOptions, - restrictTitleEditToOwner: true, + restrictTitleEditToModerator: true, }); const result = sessionPermissionLogic(s, currentUser, true, false); expect(result.canEditTitle).toBe(true); @@ -206,7 +207,7 @@ describe('Session Permission Logic', () => { it('When restricting re-ordering to owner', () => { const s = session({ ...defaultOptions, - restrictReorderingToOwner: true, + restrictReorderingToModerator: true, }); const result = sessionPermissionLogic(s, currentUser, true, false); expect(result.canReorderPosts).toBe(true); @@ -638,7 +639,7 @@ describe('Posts Permission Logic', () => { { ...defaultOptions, allowReordering: true, - restrictReorderingToOwner: true, + restrictReorderingToModerator: true, }, p ); diff --git a/frontend/src/views/game/board/header/BoardHeader.tsx b/frontend/src/views/game/board/header/BoardHeader.tsx index 18ea558fb..04391e705 100644 --- a/frontend/src/views/game/board/header/BoardHeader.tsx +++ b/frontend/src/views/game/board/header/BoardHeader.tsx @@ -1,6 +1,6 @@ import { memo, useCallback } from 'react'; import styled from '@emotion/styled'; -import { SessionOptions, ColumnDefinition } from 'common'; +import { SessionOptions, SessionSettings } from 'common'; import Typography from '@mui/material/Typography'; import { makeStyles } from '@mui/styles'; import { useTranslation } from 'react-i18next'; @@ -26,13 +26,7 @@ import { useShouldLockSession } from 'views/game/useTimer'; import ClosableAlert from 'components/ClosableAlert'; interface BoardHeaderProps { - onRenameSession: (name: string) => void; - onEditOptions: (options: SessionOptions) => void; - onEditColumns: (columns: ColumnDefinition[]) => void; - onSaveTemplate: ( - options: SessionOptions, - columns: ColumnDefinition[] - ) => void; + onChangeSession: (session: SessionSettings, saveAsTemplate: boolean) => void; onLockSession: (locked: boolean) => void; } @@ -45,13 +39,7 @@ const useStyles = makeStyles({ }, }); -function BoardHeader({ - onEditOptions, - onEditColumns, - onSaveTemplate, - onLockSession, - onRenameSession, -}: BoardHeaderProps) { +function BoardHeader({ onChangeSession, onLockSession }: BoardHeaderProps) { const { t } = useTranslation(); const classes = useStyles(); const [key] = useEncryptionKey(); @@ -74,15 +62,22 @@ function BoardHeader({ ...session.options, blurCards: false, }; - onEditOptions(modifiedOptions); + onChangeSession({ ...session, options: modifiedOptions }, false); } - }, [onEditOptions, session]); + }, [onChangeSession, session]); const handleRenameSession = useCallback( (name: string) => { - onRenameSession(encrypt(name)); + if (session) { + onChangeSession( + { + name: encrypt(name), + }, + false + ); + } }, - [onRenameSession, encrypt] + [onChangeSession, encrypt, session] ); if (!session) { @@ -131,9 +126,9 @@ function BoardHeader({ {canReveal ? : null} {canModifyOptions ? ( ) : null} diff --git a/frontend/src/views/game/board/header/ModifyOptions.tsx b/frontend/src/views/game/board/header/ModifyOptions.tsx index b6ef7f5a3..5537dca42 100644 --- a/frontend/src/views/game/board/header/ModifyOptions.tsx +++ b/frontend/src/views/game/board/header/ModifyOptions.tsx @@ -1,67 +1,48 @@ import { useState, useCallback } from 'react'; import Button from '@mui/material/Button'; import SessionEditor from '../../../session-editor/SessionEditor'; -import { ColumnSettings } from '../../../../state/types'; -import { SessionOptions, ColumnDefinition } from 'common'; -import { toColumnDefinitions } from '../../../../state/columns'; +import { AllSessionSettings, SessionSettings, User } from 'common'; import { trackEvent } from '../../../../track'; import { Settings } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; -import useSession from '../../useSession'; import { IconButton, useMediaQuery } from '@mui/material'; interface ModifyOptionsProps { - onEditOptions: (options: SessionOptions) => void; - onEditColumns: (columns: ColumnDefinition[]) => void; - onSaveTemplate: ( - options: SessionOptions, - columns: ColumnDefinition[] - ) => void; + settings: AllSessionSettings; + owner: User; + onChange: (settings: SessionSettings, saveAsTemplate: boolean) => void; } -function ModifyOptions({ - onEditOptions, - onEditColumns, - onSaveTemplate, -}: ModifyOptionsProps) { +function ModifyOptions({ settings, owner, onChange }: ModifyOptionsProps) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { session } = useSession(); + const small = useMediaQuery('(max-width: 500px)'); const handleChange = useCallback( - ( - updatedOptions: SessionOptions, - updatedColumns: ColumnSettings[], - saveAsTemplate: boolean - ) => { + (modifiedSettings: AllSessionSettings, saveAsTemplate: boolean) => { setOpen(false); - if (!session) { + if (!settings) { return; } - const { options, columns } = session; - if (options !== updatedOptions) { - onEditOptions(updatedOptions); - trackEvent('game/session/edit-options'); - } - if (columns !== updatedColumns) { - onEditColumns(toColumnDefinitions(updatedColumns)); - trackEvent('game/session/edit-columns'); - } - if (saveAsTemplate) { - onSaveTemplate(updatedOptions, toColumnDefinitions(updatedColumns)); - trackEvent('custom-modal/template/set-defaut'); - } + trackEvent('game/session/save-options'); + onChange( + { + columns: modifiedSettings.columns, + moderator: modifiedSettings.moderator, + timer: modifiedSettings.timer, + options: modifiedSettings.options, + }, + saveAsTemplate + ); }, - [onEditOptions, onEditColumns, onSaveTemplate, session] + [onChange, settings] ); - if (!session) { + if (!settings) { return null; } - const { options, columns } = session; - return ( <> {small ? ( @@ -80,10 +61,9 @@ function ModifyOptions({ )} {open ? ( setOpen(false)} onChange={handleChange} /> diff --git a/frontend/src/views/game/board/header/useCanModifyOptions.ts b/frontend/src/views/game/board/header/useCanModifyOptions.ts index ee358f692..d31887811 100644 --- a/frontend/src/views/game/board/header/useCanModifyOptions.ts +++ b/frontend/src/views/game/board/header/useCanModifyOptions.ts @@ -4,5 +4,10 @@ import useSession from '../../useSession'; export default function useCanModifyOptions(): boolean { const { session } = useSession(); const user = useUser(); - return (!!user && session && user.id === session.createdBy.id) || false; + return ( + (!!user && + session && + (user.id === session.createdBy.id || user.id === session.moderator.id)) || + false + ); } diff --git a/frontend/src/views/game/board/permissions-logic.ts b/frontend/src/views/game/board/permissions-logic.ts index 25b5db6dd..e01498b8d 100644 --- a/frontend/src/views/game/board/permissions-logic.ts +++ b/frontend/src/views/game/board/permissions-logic.ts @@ -24,7 +24,9 @@ export function sessionPermissionLogic( hasReachedMaxPosts: false, }; } - const isOwner = user.id === session.createdBy.id; + const isModerator = user.id === session.moderator.id; + const isOwner = isModerator || user.id === session.createdBy.id; + const numberOfPosts = session.posts.filter( (p) => p.user.id === user.id ).length; @@ -36,11 +38,11 @@ export function sessionPermissionLogic( const canCreateGroup = canCreatePost && session.options.allowGrouping && - (isOwner || !session.options.restrictGroupingToOwner); + (isOwner || !session.options.restrictGroupingToModerator); const canEditTitle = - !readonly && (isOwner || !session.options.restrictTitleEditToOwner); + !readonly && (isOwner || !session.options.restrictTitleEditToModerator); const canReorderPosts = - !readonly && (isOwner || !session.options.restrictReorderingToOwner); + !readonly && (isOwner || !session.options.restrictReorderingToModerator); return { canCreatePost, @@ -99,7 +101,7 @@ export function postPermissionLogic( allowGiphy, allowReordering, allowCancelVote, - restrictReorderingToOwner, + restrictReorderingToModerator: restrictReorderingToOwner, blurCards, } = session.options; diff --git a/frontend/src/views/game/useGame.ts b/frontend/src/views/game/useGame.ts index dbd7e4978..26e629f48 100644 --- a/frontend/src/views/game/useGame.ts +++ b/frontend/src/views/game/useGame.ts @@ -4,16 +4,12 @@ import { Post, PostGroup, VoteType, - SessionOptions, - ColumnDefinition, Participant, UnauthorizedAccessPayload, WsLikeUpdatePayload, WsPostUpdatePayload, WsDeletePostPayload, WsDeleteGroupPayload, - WsNameData, - WsSaveTemplatePayload, VoteExtract, WsReceiveLikeUpdatePayload, WsErrorPayload, @@ -26,6 +22,9 @@ import { WsCancelVotesPayload, WsReceiveCancelVotesPayload, WsReceiveTimerStartPayload, + WsSaveSessionSettingsPayload, + SessionSettings, + AllSessionSettings, } from 'common'; import { v4 } from 'uuid'; import find from 'lodash/find'; @@ -127,10 +126,8 @@ function useGame(sessionId: string) { deletePostGroup, updatePostGroup, receiveVote, - renameSession, resetSession, - editOptions, - editColumns, + editSessionSettings, lockSession, userReady, cancelVotes, @@ -286,20 +283,6 @@ function useGame(sessionId: string) { setStatus('connected'); }); - socket.on(Actions.RECEIVE_OPTIONS, (options: SessionOptions) => { - if (debug) { - console.log('Receive updated options: ', options); - } - editOptions(options); - }); - - socket.on(Actions.RECEIVE_COLUMNS, (columns: ColumnDefinition[]) => { - if (debug) { - console.log('Receive updated columns: ', columns); - } - editColumns(columns); - }); - socket.on(Actions.RECEIVE_CLIENT_LIST, (participants: Participant[]) => { if (debug) { console.log('Receive participants list: ', participants); @@ -360,13 +343,6 @@ function useGame(sessionId: string) { updatePostGroup(group); }); - socket.on(Actions.RECEIVE_SESSION_NAME, (name: string) => { - if (debug) { - console.log('Receive session name: ', name); - } - renameSession(name); - }); - socket.on(Actions.RECEIVE_LOCK_SESSION, (locked: boolean) => { if (debug) { console.log('Receive lock session: ', locked); @@ -436,6 +412,16 @@ function useGame(sessionId: string) { } setTimer(null); }); + + socket.on( + Actions.RECEIVE_SESSION_SETTINGS, + (payload: Partial) => { + if (debug) { + console.log('Receive session settings', payload); + } + editSessionSettings(payload); + } + ); }, [ socket, status, @@ -450,18 +436,16 @@ function useGame(sessionId: string) { updateParticipants, deletePost, updatePost, - editOptions, - editColumns, receivePostGroup, deletePostGroup, updatePostGroup, - renameSession, lockSession, enqueueSnackbar, setUnauthorised, userReady, cancelVotes, setTimer, + editSessionSettings, userId, ]); @@ -734,50 +718,18 @@ function useGame(sessionId: string) { [user, send, updatePost, allowCancelVotes] ); - const onRenameSession = useCallback( - (name: string) => { - if (send) { - renameSession(name); - send(Actions.RENAME_SESSION, { name }); - trackAction(Actions.RENAME_SESSION); - } - }, - [send, renameSession] - ); - - const onEditOptions = useCallback( - (options: SessionOptions) => { - if (send) { - editOptions(options); - send(Actions.EDIT_OPTIONS, options); - trackAction(Actions.EDIT_OPTIONS); - } - }, - [send, editOptions] - ); - - const onEditColumns = useCallback( - (columns: ColumnDefinition[]) => { - if (send) { - editColumns(columns); - send(Actions.EDIT_COLUMNS, columns); - trackAction(Actions.EDIT_COLUMNS); - } - }, - [send, editColumns] - ); - - const onSaveTemplate = useCallback( - (options: SessionOptions, columns: ColumnDefinition[]) => { + const onChangeSession = useCallback( + (settings: SessionSettings, saveAsTemplate: boolean) => { if (send) { - send(Actions.SAVE_TEMPLATE, { - options, - columns, + editSessionSettings(settings); + send(Actions.SAVE_SESSION_SETTINGS, { + settings, + saveAsTemplate, }); - trackAction(Actions.SAVE_TEMPLATE); + trackAction(Actions.SAVE_SESSION_SETTINGS); } }, - [send] + [send, editSessionSettings] ); const onLockSession = useCallback( @@ -829,10 +781,7 @@ function useGame(sessionId: string) { onDeletePostGroup, onLike, onCancelVotes, - onRenameSession, - onEditOptions, - onEditColumns, - onSaveTemplate, + onChangeSession, onLockSession, reconnect, onUserReady, diff --git a/frontend/src/views/game/useSession.ts b/frontend/src/views/game/useSession.ts index ffaf77dae..8266be1f3 100644 --- a/frontend/src/views/game/useSession.ts +++ b/frontend/src/views/game/useSession.ts @@ -1,11 +1,10 @@ import { - ColumnDefinition, Post, PostGroup, Session, VoteExtract, - SessionOptions, Message, + SessionSettings, } from 'common'; import { findIndex } from 'lodash'; import { useCallback } from 'react'; @@ -14,7 +13,6 @@ import { SessionState } from './state'; interface UseSession { session: Session | null; - renameSession: (name: string) => void; resetSession: () => void; receivePost: (post: Post) => void; receivePostGroup: (postGroup: PostGroup) => void; @@ -25,8 +23,7 @@ interface UseSession { receiveVote: (postId: string, vote: VoteExtract) => void; deletePost: (postId: string) => void; deletePostGroup: (groupId: string) => void; - editOptions: (options: SessionOptions) => void; - editColumns: (columns: ColumnDefinition[]) => void; + editSessionSettings: (updated: SessionSettings) => void; lockSession: (locked: boolean) => void; userReady: (userId: string, ready?: boolean) => void; cancelVotes: (postId: string, userId: string) => void; @@ -35,20 +32,6 @@ interface UseSession { export default function useSession(): UseSession { const [session, setSession] = useRecoilState(SessionState); - const renameSession = useCallback( - (name: string) => { - setSession((session) => - !session - ? session - : { - ...session, - name, - } - ); - }, - [setSession] - ); - const resetSession = useCallback(() => { setSession(null); }, [setSession]); @@ -233,27 +216,19 @@ export default function useSession(): UseSession { }, [setSession] ); - const editOptions = useCallback( - (options: SessionOptions) => { - setSession((session) => - !session - ? session - : { - ...session, - options, - } - ); - }, - [setSession] - ); - const editColumns = useCallback( - (columns: ColumnDefinition[]) => { + + const editSessionSettings = useCallback( + (updated: SessionSettings) => { setSession((session) => !session - ? session + ? null : { ...session, - columns, + name: checkUndefined(updated.name, session.name), + options: checkUndefined(updated.options, session.options), + columns: checkUndefined(updated.columns, session.columns), + locked: checkUndefined(updated.locked, session.locked), + moderator: checkUndefined(updated.moderator, session.moderator), } ); }, @@ -294,7 +269,6 @@ export default function useSession(): UseSession { return { session, - renameSession, resetSession, receiveBoard, receivePost, @@ -305,10 +279,16 @@ export default function useSession(): UseSession { updatePostGroup, deletePost, deletePostGroup, - editColumns, - editOptions, + editSessionSettings, lockSession, userReady, cancelVotes, }; } + +function checkUndefined(value: T | undefined, defaultValue: T): T { + if (value === undefined) { + return defaultValue; + } + return value; +} diff --git a/frontend/src/views/home/CreateSession.tsx b/frontend/src/views/home/CreateSession.tsx deleted file mode 100644 index 6aa4f638b..000000000 --- a/frontend/src/views/home/CreateSession.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { SessionOptions, ColumnDefinition, defaultOptions } from 'common'; -import { buildDefaults, toColumnDefinitions } from '../../state/columns'; -import { ColumnSettings } from '../../state/types'; -import { useTranslation } from 'react-i18next'; -import { trackEvent } from './../../track'; -import SessionEditor from '../session-editor/SessionEditor'; - -interface CreateSessionModalProps { - open: boolean; - onClose: () => void; - onLaunch: ( - options: SessionOptions, - columns: ColumnDefinition[], - makeDefault: boolean - ) => void; -} - -const CreateSessionModal = ({ - open, - onClose, - onLaunch, -}: CreateSessionModalProps) => { - const { t } = useTranslation(); - const defaultDefinitions = useMemo(() => { - return buildDefaults('default', t); - }, [t]); - - const handleChange = useCallback( - ( - options: SessionOptions, - columns: ColumnSettings[], - makeDefault: boolean - ) => { - trackEvent('custom-modal/create'); - if (makeDefault) { - trackEvent('custom-modal/template/set-defaut'); - } - onLaunch(options, toColumnDefinitions(columns), makeDefault); - }, - [onLaunch] - ); - - return ( - - ); -}; - -export default CreateSessionModal; diff --git a/frontend/src/views/session-editor/SessionEditor.tsx b/frontend/src/views/session-editor/SessionEditor.tsx index b5ae9a147..ff8d9526e 100644 --- a/frontend/src/views/session-editor/SessionEditor.tsx +++ b/frontend/src/views/session-editor/SessionEditor.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useCallback } from 'react'; -import { SessionOptions } from 'common'; +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { AllSessionSettings, SessionOptions, User } from 'common'; import Dialog from '@mui/material/Dialog'; import DialogContent from '@mui/material/DialogContent'; import useMediaQuery from '@mui/material/useMediaQuery'; @@ -16,57 +16,60 @@ import { ColumnSettings } from '../../state/types'; import TemplateSection from './sections/template/TemplateSection'; import PostsSection from './sections/posts/PostsSection'; import VotingSection from './sections/votes/VotingSection'; -import { extrapolate, hasChanged } from '../../state/columns'; +import { extrapolate, toColumnDefinitions } from '../../state/columns'; import TimerSection from './sections/timer/TimerSection'; +import BoardSection from './sections/board/BoardSection'; interface SessionEditorProps { open: boolean; - options: SessionOptions; - columns: ColumnSettings[]; - edit?: boolean; - onChange: ( - options: SessionOptions, - columns: ColumnSettings[], - makeDefault: boolean - ) => void; + owner: User; + settings: AllSessionSettings; + onChange: (settings: AllSessionSettings, makeDefault: boolean) => void; onClose: () => void; } function SessionEditor({ open, - options: incomingOptions, - columns, - edit = false, + owner, + settings: originalSettings, onChange, onClose, }: SessionEditorProps) { const { t } = useTranslation(); const fullScreen = useMediaQuery('(max-width:600px)'); const [isDefaultTemplate, toggleIsDefaultTemplate] = useToggle(false); - const [definitions, setDefinitions] = useState(columns); - const [options, setOptions] = useState(incomingOptions); + const [settings, setSettings] = useState(originalSettings); const [currentTab, setCurrentTab] = useState('template'); - useEffect(() => { - const extrapolatedColumns = columns.map((c) => extrapolate(c, t)); - setDefinitions(extrapolatedColumns); - }, [columns, t]); + const extrapolatedColumns = useMemo(() => { + const extrapolatedColumns = settings.columns.map((c) => extrapolate(c, t)); + return extrapolatedColumns; + }, [settings.columns, t]); useEffect(() => { - setOptions(incomingOptions); - }, [incomingOptions]); + setSettings(originalSettings); + }, [originalSettings]); const handleCreate = useCallback(() => { - const definitionsToPersist = hasChanged(columns, definitions, t) - ? definitions - : columns; - onChange(options, definitionsToPersist, isDefaultTemplate); - }, [onChange, options, definitions, isDefaultTemplate, columns, t]); + onChange(settings, isDefaultTemplate); + }, [onChange, isDefaultTemplate, settings]); const handleTab = useCallback((_: React.ChangeEvent<{}>, value: string) => { setCurrentTab(value); }, []); + const handleOptionsChange = useCallback((options: SessionOptions) => { + setSettings((prev) => ({ ...prev, options })); + }, []); + + const handleSettingsChange = useCallback((options: AllSessionSettings) => { + setSettings(options); + }, []); + + const handleColumnsChanged = useCallback((columns: ColumnSettings[]) => { + setSettings((prev) => ({ ...prev, columns: toColumnDefinitions(columns) })); + }, []); + return ( + @@ -95,16 +99,35 @@ function SessionEditor({ {currentTab === 'template' ? ( - + + ) : null} + {currentTab === 'board' ? ( + ) : null} {currentTab === 'posts' ? ( - + ) : null} {currentTab === 'voting' ? ( - + ) : null} {currentTab === 'timer' ? ( - + ) : null} @@ -121,7 +144,7 @@ function SessionEditor({ {t('Generic.cancel')} diff --git a/frontend/src/views/session-editor/sections/board/BoardSection.tsx b/frontend/src/views/session-editor/sections/board/BoardSection.tsx new file mode 100644 index 000000000..0371dafcc --- /dev/null +++ b/frontend/src/views/session-editor/sections/board/BoardSection.tsx @@ -0,0 +1,97 @@ +import { useCallback, useEffect, useState } from 'react'; +import SettingCategory from '../SettingCategory'; +import { useTranslation } from 'react-i18next'; +import { AllSessionSettings, User } from 'common'; +import { OptionItem } from '../OptionItem'; +import BooleanOption from '../BooleanOption'; +import { fetchUsers } from './api'; +import { UserSelector } from './UserSelector'; +import CustomAvatar from 'components/Avatar'; +import styled from '@emotion/styled'; + +interface BoardSectionProps { + owner: User; + options: AllSessionSettings; + onChange: (options: AllSessionSettings) => void; +} + +function BoardSection({ options, owner, onChange }: BoardSectionProps) { + const { t } = useTranslation(); + const [users, setUsers] = useState([]); + + useEffect(() => { + async function load() { + const users = await fetchUsers(); + setUsers(users); + } + load(); + }, []); + + const setRestrictTitleEditToOwner = useCallback( + (value: boolean) => { + onChange({ + ...options, + options: { + ...options.options, + restrictTitleEditToModerator: value, + }, + }); + }, + [onChange, options] + ); + + const setModerator = useCallback( + (user: User) => { + onChange({ + ...options, + moderator: user, + }); + }, + [onChange, options] + ); + + return ( + + + + {owner.name} + + + + + + + + + + + ); +} + +const Line = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +export default BoardSection; diff --git a/frontend/src/views/session-editor/sections/board/UserSelector.tsx b/frontend/src/views/session-editor/sections/board/UserSelector.tsx new file mode 100644 index 000000000..b3ddd92de --- /dev/null +++ b/frontend/src/views/session-editor/sections/board/UserSelector.tsx @@ -0,0 +1,96 @@ +import styled from '@emotion/styled'; +import { Autocomplete, TextField, colors } from '@mui/material'; +import { User } from 'common'; +import CustomAvatar from 'components/Avatar'; +import { useMemo } from 'react'; + +type UserSelectorProps = { + moderatorId: string; + options: User[]; + onSelect: (user: User) => void; +}; + +export function UserSelector({ + moderatorId, + options, + onSelect, +}: UserSelectorProps) { + const selected: User | null = useMemo(() => { + return options.find((u) => u.id === moderatorId) || null; + }, [options, moderatorId]); + + return ( + + 0} + onChange={(_, value) => onSelect(value as User)} + fullWidth + renderInput={(params) => ( + + )} + renderOption={(props, option) => ( + + + + )} + getOptionLabel={(option) => option.name} + /> + + ); +} + +type UserItemProps = { + user: User; +}; + +export function UserItem({ user }: UserItemProps) { + return ( + + + + + {user.name} + {user.email} + + ); +} + +const Container = styled.div` + display: grid; + grid-template-columns: 50px 1fr; + grid-template-rows: auto auto; + grid-template-areas: + 'avatar name' + 'avatar email'; + row-gap: 1px; + font-size: 0.8rem; + align-items: center; +`; + +const Avatar = styled.div` + grid-area: avatar; +`; +const Name = styled.div` + grid-area: name; +`; +const Email = styled.div` + grid-area: email; + color: ${colors.grey[500]}; +`; +const SelectorContainer = styled.div` + display: flex; + justify-content: flex-end; + > * { + margin: 5px 0; + } + + @media screen and (min-width: 600px) { + > * { + max-width: 300px; + } + } +`; diff --git a/frontend/src/views/session-editor/sections/board/api.ts b/frontend/src/views/session-editor/sections/board/api.ts new file mode 100644 index 000000000..2559a4db5 --- /dev/null +++ b/frontend/src/views/session-editor/sections/board/api.ts @@ -0,0 +1,6 @@ +import { fetchGet } from 'api/fetch'; +import { User } from 'common'; + +export async function fetchUsers() { + return await fetchGet('/api/users', []); +} diff --git a/frontend/src/views/session-editor/sections/posts/PostsSection.tsx b/frontend/src/views/session-editor/sections/posts/PostsSection.tsx index 4745d4d24..e8e5de4dc 100644 --- a/frontend/src/views/session-editor/sections/posts/PostsSection.tsx +++ b/frontend/src/views/session-editor/sections/posts/PostsSection.tsx @@ -100,7 +100,7 @@ function PostsSection({ options, onChange }: PostsSectionProps) { (value: boolean) => { onChange({ ...options, - restrictReorderingToOwner: value, + restrictReorderingToModerator: value, }); }, [onChange, options] @@ -110,17 +110,7 @@ function PostsSection({ options, onChange }: PostsSectionProps) { (value: boolean) => { onChange({ ...options, - restrictGroupingToOwner: value, - }); - }, - [onChange, options] - ); - - const setRestrictTitleEditToOwner = useCallback( - (value: boolean) => { - onChange({ - ...options, - restrictTitleEditToOwner: value, + restrictGroupingToModerator: value, }); }, [onChange, options] @@ -138,15 +128,6 @@ function PostsSection({ options, onChange }: PostsSectionProps) { > - - - @@ -211,7 +192,7 @@ function PostsSection({ options, onChange }: PostsSectionProps) { help={t('Customize.restrictGroupingToOwnerHelp')!} > diff --git a/retro-board-dev/docker-compose.yml b/retro-board-dev/docker-compose.yml index 23113d049..8c2a49a4c 100644 --- a/retro-board-dev/docker-compose.yml +++ b/retro-board-dev/docker-compose.yml @@ -13,13 +13,13 @@ services: max-size: '50m' postgres: - image: postgres:11.6 + image: postgres:16 hostname: postgres ports: - '5432:5432' environment: POSTGRES_USER: postgres # Must be the same as DB_USER below - POSTGRES_PASSWORD: postgres # Must be the same as DB_PASSWORD below + POSTGRES_PASSWORD: postgres # Must be the password provided to the backend POSTGRES_DB: retroboard # Must be the same as DB_NAME below volumes: - database_dev:/var/lib/postgresql/data @@ -30,13 +30,13 @@ services: max-size: '50m' pgadmin: - image: dpage/pgadmin4:4.15 # use biarms/pgadmin4 on ARM + image: dpage/pgadmin4:latest depends_on: - postgres ports: - '5433:80' # Change 8080 to whatever port you want to access pgAdmin from environment: - PGADMIN_DEFAULT_EMAIL: admin + PGADMIN_DEFAULT_EMAIL: admin@admin.com PGADMIN_DEFAULT_PASSWORD: admin volumes: - pgadmin_dev:/var/lib/pgadmin