diff --git a/codegen.json b/codegen.json index 999ac8008ba..5774ea539d6 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,12 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource", + "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource", + "RemoveReflectTemplatePromptPayload": "./types/RemoveReflectTemplatePromptPayload#RemoveReflectTemplatePromptPayloadSource", + "RenameReflectTemplatePromptPayload": "./types/RenameReflectTemplatePromptPayload#RenameReflectTemplatePromptPayloadSource", + "MoveReflectTemplatePromptPayload": "./types/MoveReflectTemplatePromptPayload#MoveReflectTemplatePromptPayloadSource", + "AddReflectTemplatePromptPayload": "./types/AddReflectTemplatePromptPayload#AddReflectTemplatePromptPayloadSource", "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", "AddCommentSuccess": "./types/AddCommentSuccess#AddCommentSuccessSource", diff --git a/packages/server/database/types/RetrospectivePrompt.ts b/packages/server/database/types/RetrospectivePrompt.ts index a278dee85ad..23f8b6b5373 100644 --- a/packages/server/database/types/RetrospectivePrompt.ts +++ b/packages/server/database/types/RetrospectivePrompt.ts @@ -8,7 +8,7 @@ interface Input { description: string groupColor: string removedAt: Date | null - parentPromptId?: string + parentPromptId?: string | null } export default class RetrospectivePrompt { @@ -22,7 +22,7 @@ export default class RetrospectivePrompt { question: string removedAt: Date | null updatedAt = new Date() - parentPromptId?: string + parentPromptId?: string | null constructor(input: Input) { const { @@ -43,6 +43,6 @@ export default class RetrospectivePrompt { this.description = description || '' this.groupColor = groupColor this.removedAt = removedAt - this.parentPromptId = parentPromptId + this.parentPromptId = parentPromptId || null } } diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 0f08e21d5a6..2c2381b23a9 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -4,6 +4,7 @@ import { selectAgendaItems, selectComments, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -215,3 +216,14 @@ export const commentsByDiscussionId = foreignKeyLoaderMaker( return selectComments().where('discussionId', 'in', discussionIds).execute() } ) + +export const _pgreflectPromptsByTemplateId = foreignKeyLoaderMaker( + '_pgreflectPrompts', + 'templateId', + async (templateIds) => { + return selectReflectPrompts() + .where('templateId', 'in', templateIds) + .orderBy('sortOrder') + .execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index adeee70820a..e80d3785659 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -10,6 +10,7 @@ import { selectComments, selectMeetingSettings, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -110,3 +111,7 @@ export const slackNotifications = primaryKeyLoaderMaker((ids: readonly string[]) export const comments = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectComments().where('id', 'in', ids).execute() }) + +export const _pgreflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectReflectPrompts().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/mutations/addReflectTemplatePrompt.ts deleted file mode 100644 index e0867254b79..00000000000 --- a/packages/server/graphql/mutations/addReflectTemplatePrompt.ts +++ /dev/null @@ -1,90 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' -import dndNoise from 'parabol-client/utils/dndNoise' -import palettePickerOptions from '../../../client/styles/palettePickerOptions' -import {PALETTE} from '../../../client/styles/paletteV3' -import getRethink from '../../database/rethinkDriver' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import AddReflectTemplatePromptPayload from '../types/AddReflectTemplatePromptPayload' - -const addReflectTemplatePrompt = { - description: 'Add a new template full of prompts', - type: AddReflectTemplatePromptPayload, - args: { - templateId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {templateId}: {templateId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const template = await dataLoader.get('meetingTemplates').load(templateId) - const viewerId = getUserId(authToken) - - // AUTH - if (!template || !template.isActive) { - return standardError(new Error('Template not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, template.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId} = template - const activePrompts = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - templateId, - removedAt: null - }) - .run() - if (activePrompts.length >= Threshold.MAX_REFLECTION_PROMPTS) { - return standardError(new Error('Too many prompts'), {userId: viewerId}) - } - - // RESOLUTION - const sortOrder = - Math.max(0, ...activePrompts.map((prompt) => prompt.sortOrder)) + 1 + dndNoise() - const pickedColors = activePrompts.map((prompt) => prompt.groupColor) - const availableNewColor = palettePickerOptions.find( - (color) => !pickedColors.includes(color.hex) - ) - const reflectPrompt = new RetrospectivePrompt({ - templateId: template.id, - teamId: template.teamId, - sortOrder, - question: `New prompt #${activePrompts.length + 1}`, - description: '', - groupColor: availableNewColor?.hex ?? PALETTE.JADE_400, - removedAt: null - }) - - await Promise.all([ - await r.table('ReflectPrompt').insert(reflectPrompt).run(), - pg - .updateTable('MeetingTemplate') - .set({updatedAt: new Date()}) - .where('id', '=', templateId) - .execute() - ]) - - const promptId = reflectPrompt.id - const data = {promptId} - publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePromptPayload', data, subOptions) - return data - } -} - -export default addReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts b/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts index bd57b97b0c8..2c52d8eadff 100644 --- a/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts +++ b/packages/server/graphql/mutations/helpers/makeRetroTemplates.ts @@ -1,5 +1,7 @@ +import {positionAfter} from '../../../../client/shared/sortOrder' import ReflectTemplate from '../../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' +import generateUID from '../../../generateUID' +import {ReflectPrompt} from '../../../postgres/types' import getTemplateIllustrationUrl from './getTemplateIllustrationUrl' interface TemplatePrompt { @@ -14,7 +16,7 @@ interface TemplateObject { } const makeRetroTemplates = (teamId: string, orgId: string, templateObj: TemplateObject) => { - const reflectPrompts: RetrospectivePrompt[] = [] + const reflectPrompts: ReflectPrompt[] = [] const templates: ReflectTemplate[] = [] Object.entries(templateObj).forEach(([templateName, promptBase]) => { const template = new ReflectTemplate({ @@ -25,18 +27,24 @@ const makeRetroTemplates = (teamId: string, orgId: string, templateObj: Template mainCategory: 'retrospective' }) - const prompts = promptBase.map( - (prompt, idx) => - new RetrospectivePrompt({ - teamId, - templateId: template.id, - sortOrder: idx, - question: prompt.question, - description: prompt.description, - groupColor: prompt.groupColor, - removedAt: null - }) - ) + let curSortOrder = positionAfter('') + const prompts = promptBase.map((prompt) => { + curSortOrder = positionAfter(curSortOrder) + return { + id: generateUID(), + teamId, + templateId: template.id, + sortOrder: curSortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, + removedAt: null, + parentPromptId: null, + // can remove these after phase 3 + createdAt: new Date(), + updatedAt: new Date() + } + }) templates.push(template) reflectPrompts.push(...prompts) }) diff --git a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts deleted file mode 100644 index 2a02c13f9d7..00000000000 --- a/packages/server/graphql/mutations/moveReflectTemplatePrompt.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {GraphQLFloat, GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import MoveReflectTemplatePromptPayload from '../types/MoveReflectTemplatePromptPayload' - -const moveReflectTemplate = { - description: 'Move a reflect template', - type: MoveReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - sortOrder: { - type: new GraphQLNonNull(GraphQLFloat) - } - }, - async resolve( - _source: unknown, - {promptId, sortOrder}: {promptId: string; sortOrder: number}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // RESOLUTION - const {teamId, templateId} = prompt - - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - sortOrder, - updatedAt: now - }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() - ]) - - const data = {promptId} - publish(SubscriptionChannel.TEAM, teamId, 'MoveReflectTemplatePromptPayload', data, subOptions) - return data - } -} - -export default moveReflectTemplate diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts b/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts deleted file mode 100644 index 73254ea5522..00000000000 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateDescription.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import ReflectTemplatePromptUpdateDescriptionPayload from '../types/ReflectTemplatePromptUpdateDescriptionPayload' - -const reflectTemplatePromptUpdateDescription = { - description: 'Update the description of a reflection prompt', - type: ReflectTemplatePromptUpdateDescriptionPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - description: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, description}: {promptId: string; description: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - const normalizedDescription = description.trim().slice(0, 256) || '' - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - description: normalizedDescription, - updatedAt: now - }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() - ]) - - const data = {promptId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'ReflectTemplatePromptUpdateDescriptionPayload', - data, - subOptions - ) - return data - } -} - -export default reflectTemplatePromptUpdateDescription diff --git a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts b/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts deleted file mode 100644 index c7c397d1a99..00000000000 --- a/packages/server/graphql/mutations/reflectTemplatePromptUpdateGroupColor.ts +++ /dev/null @@ -1,72 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import ReflectTemplatePromptUpdateGroupColorPayload from '../types/ReflectTemplatePromptUpdateGroupColorPayload' - -const reflectTemplatePromptUpdateGroupColor = { - groupColor: 'Update the groupColor of a reflection prompt', - type: ReflectTemplatePromptUpdateGroupColorPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - groupColor: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, groupColor}: {promptId: string; groupColor: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const viewerId = getUserId(authToken) - - const prompt = await r.table('ReflectPrompt').get(promptId).run() - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - groupColor, - updatedAt: now - }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() - ]) - - const data = {promptId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'ReflectTemplatePromptUpdateGroupColorPayload', - data, - subOptions - ) - return data - } -} - -export default reflectTemplatePromptUpdateGroupColor diff --git a/packages/server/graphql/mutations/removeReflectTemplate.ts b/packages/server/graphql/mutations/removeReflectTemplate.ts index 3357551a51a..14224dac4da 100644 --- a/packages/server/graphql/mutations/removeReflectTemplate.ts +++ b/packages/server/graphql/mutations/removeReflectTemplate.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' -import removeMeetingTemplate from '../../postgres/queries/removeMeetingTemplate' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -23,6 +22,7 @@ const removeReflectTemplate = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const r = await getRethink() + const pg = getKysely() const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -47,7 +47,6 @@ const removeReflectTemplate = { // RESOLUTION const {id: settingsId} = settings await Promise.all([ - removeMeetingTemplate(templateId), r .table('ReflectPrompt') .getAll(teamId, {index: 'teamId'}) @@ -58,9 +57,17 @@ const removeReflectTemplate = { removedAt: now, updatedAt: now }) - .run() + .run(), + pg + .with('RemoveTemplate', (qb) => + qb.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) + ) + .updateTable('ReflectPrompt') + .set({removedAt: now}) + .where('templateId', '=', templateId) + .execute() ]) - + dataLoader.clearAll('reflectPrompts') if (settings.selectedTemplateId === templateId) { const nextTemplate = templates.find((template) => template.id !== templateId) const nextTemplateId = nextTemplate?.id ?? 'workingStuckTemplate' diff --git a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts deleted file mode 100644 index b0f2f1eebaf..00000000000 --- a/packages/server/graphql/mutations/removeReflectTemplatePrompt.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RemoveReflectTemplatePromptPayload from '../types/RemoveReflectTemplatePromptPayload' - -const removeReflectTemplatePrompt = { - description: 'Remove a prompt from a template', - type: RemoveReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {promptId}: {promptId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - const promptCount = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - removedAt: null, - templateId: templateId - }) - .count() - .default(0) - .run() - - if (promptCount <= 1) { - return standardError(new Error('No prompts remain'), {userId: viewerId}) - } - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - removedAt: now, - updatedAt: now - }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() - ]) - - const data = {promptId, templateId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'RemoveReflectTemplatePromptPayload', - data, - subOptions - ) - return data - } -} - -export default removeReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts deleted file mode 100644 index b1a74470ebc..00000000000 --- a/packages/server/graphql/mutations/renameReflectTemplatePrompt.ts +++ /dev/null @@ -1,85 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RenameReflectTemplatePromptPayload from '../types/RenameReflectTemplatePromptPayload' - -const renameReflectTemplatePrompt = { - description: 'Rename a reflect template prompt', - type: RenameReflectTemplatePromptPayload, - args: { - promptId: { - type: new GraphQLNonNull(GraphQLID) - }, - question: { - type: new GraphQLNonNull(GraphQLString) - } - }, - async resolve( - _source: unknown, - {promptId, question}: {promptId: string; question: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const pg = getKysely() - const now = new Date() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const prompt = await r.table('ReflectPrompt').get(promptId).run() - const viewerId = getUserId(authToken) - - // AUTH - if (!prompt || prompt.removedAt) { - return standardError(new Error('Prompt not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, prompt.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId, templateId} = prompt - const trimmedQuestion = question.trim().slice(0, 100) - const normalizedQuestion = trimmedQuestion || 'Unnamed Prompt' - - const allPrompts = await r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - removedAt: null, - templateId - }) - .run() - if (allPrompts.find((prompt) => prompt.question === normalizedQuestion)) { - return standardError(new Error('Duplicate question template'), {userId: viewerId}) - } - - // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - question: normalizedQuestion, - updatedAt: now - }) - .run(), - pg.updateTable('MeetingTemplate').set({updatedAt: now}).where('id', '=', templateId).execute() - ]) - - const data = {promptId} - publish( - SubscriptionChannel.TEAM, - teamId, - 'RenameReflectTemplatePromptPayload', - data, - subOptions - ) - return data - } -} - -export default renameReflectTemplatePrompt diff --git a/packages/server/graphql/mutations/updateTemplateScope.ts b/packages/server/graphql/mutations/updateTemplateScope.ts index b11f778856d..1b7c7aeeca1 100644 --- a/packages/server/graphql/mutations/updateTemplateScope.ts +++ b/packages/server/graphql/mutations/updateTemplateScope.ts @@ -5,7 +5,6 @@ import {RDatum} from '../../database/stricterR' import {SharingScopeEnum as ESharingScope} from '../../database/types/MeetingTemplate' import PokerTemplate from '../../database/types/PokerTemplate' import ReflectTemplate from '../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' @@ -89,12 +88,17 @@ const updateTemplateScope = { const activePrompts = prompts.filter(({removedAt}) => !removedAt) const promptIds = activePrompts.map(({id}) => id) const clonedPrompts = activePrompts.map((prompt) => { - return new RetrospectivePrompt({ - ...prompt, + return { + id: generateUID(), + teamId: prompt.teamId, templateId: clonedTemplateId!, parentPromptId: prompt.id, + sortOrder: prompt.sortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, removedAt: null - }) + } }) await Promise.all([ pg @@ -103,7 +107,13 @@ const updateTemplateScope = { ) .with('MeetingTemplateDeactivate', (qc) => qc.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) - ), + ) + .with('RemovePrompts', (qc) => + qc.updateTable('ReflectPrompt').set({removedAt: now}).where('id', 'in', promptIds) + ) + .insertInto('ReflectPrompt') + .values(clonedPrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .execute(), r.table('ReflectPrompt').insert(clonedPrompts).run(), r.table('ReflectPrompt').getAll(r.args(promptIds)).update({removedAt: now}).run() ]) diff --git a/packages/server/graphql/public/mutations/addReflectTemplate.ts b/packages/server/graphql/public/mutations/addReflectTemplate.ts index 586a6e57f05..993abe9dc0f 100644 --- a/packages/server/graphql/public/mutations/addReflectTemplate.ts +++ b/packages/server/graphql/public/mutations/addReflectTemplate.ts @@ -2,9 +2,9 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {PALETTE} from '../../../../client/styles/paletteV3' import getRethink from '../../../database/rethinkDriver' import ReflectTemplate from '../../../database/types/ReflectTemplate' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import decrementFreeTemplatesRemaining from '../../../postgres/queries/decrementFreeTemplatesRemaining' -import insertMeetingTemplate from '../../../postgres/queries/insertMeetingTemplate' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember, isUserInOrg} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -18,6 +18,7 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( {teamId, parentTemplateId}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -76,20 +77,27 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( mainCategory: parentTemplate.mainCategory }) const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(parentTemplate.id) - const activePrompts = prompts.filter(({removedAt}: RetrospectivePrompt) => !removedAt) - const newTemplatePrompts = activePrompts.map((prompt: RetrospectivePrompt) => { - return new RetrospectivePrompt({ - ...prompt, + const activePrompts = prompts.filter(({removedAt}) => !removedAt) + const newTemplatePrompts = activePrompts.map((prompt) => { + return { + id: generateUID(), teamId, templateId: newTemplate.id, parentPromptId: prompt.id, + sortOrder: prompt.sortOrder, + question: prompt.question, + description: prompt.description, + groupColor: prompt.groupColor, removedAt: null - }) + } }) - await Promise.all([ r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate), + pg + .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) + .insertInto('ReflectPrompt') + .values(newTemplatePrompts.map((p) => ({...p, sortOrder: String(p.sortOrder)}))) + .execute(), decrementFreeTemplatesRemaining(viewerId, 'retro') ]) viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 @@ -113,8 +121,15 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( const newTemplate = templates[0]! const {id: templateId} = newTemplate await Promise.all([ - r.table('ReflectPrompt').insert(newTemplatePrompts).run(), - insertMeetingTemplate(newTemplate), + r + .table('ReflectPrompt') + .insert(newTemplatePrompts.map((p, idx) => ({...p, sortOrder: idx}))) + .run(), + pg + .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) + .insertInto('ReflectPrompt') + .values(newTemplatePrompts) + .execute(), decrementFreeTemplatesRemaining(viewerId, 'retro') ]) viewer.freeCustomRetroTemplatesRemaining = viewer.freeCustomRetroTemplatesRemaining - 1 diff --git a/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts new file mode 100644 index 00000000000..43de3440ea8 --- /dev/null +++ b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts @@ -0,0 +1,75 @@ +import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' +import dndNoise from 'parabol-client/utils/dndNoise' +import {positionAfter} from '../../../../client/shared/sortOrder' +import palettePickerOptions from '../../../../client/styles/palettePickerOptions' +import {PALETTE} from '../../../../client/styles/paletteV3' +import getRethink from '../../../database/rethinkDriver' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const addReflectTemplatePrompt: MutationResolvers['addReflectTemplatePrompt'] = async ( + _source, + {templateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const template = await dataLoader.get('meetingTemplates').load(templateId) + const viewerId = getUserId(authToken) + + // AUTH + if (!template || !template.isActive) { + return standardError(new Error('Template not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, template.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId} = template + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter(({removedAt}) => !removedAt) + + if (activePrompts.length >= Threshold.MAX_REFLECTION_PROMPTS) { + return standardError(new Error('Too many prompts'), {userId: viewerId}) + } + + // RESOLUTION + const lastPrompt = activePrompts.at(-1)! + const sortOrder = lastPrompt.sortOrder + 1 + dndNoise() + // can remove String coercion after ReflectPrompt is in PG + const pgSortOrder = positionAfter(String(lastPrompt.sortOrder)) + const pickedColors = activePrompts.map((prompt) => prompt.groupColor) + const availableNewColor = palettePickerOptions.find((color) => !pickedColors.includes(color.hex)) + const reflectPrompt = { + id: generateUID(), + templateId: template.id, + teamId: template.teamId, + sortOrder, + question: `New prompt #${activePrompts.length + 1}`, + description: '', + groupColor: availableNewColor?.hex ?? PALETTE.JADE_400, + removedAt: null + } + + await Promise.all([ + r.table('ReflectPrompt').insert(reflectPrompt).run(), + pg + .insertInto('ReflectPrompt') + .values({...reflectPrompt, sortOrder: pgSortOrder}) + .execute() + ]) + dataLoader.clearAll('reflectPrompts') + const promptId = reflectPrompt.id + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'AddReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default addReflectTemplatePrompt diff --git a/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts new file mode 100644 index 00000000000..f3a43d60055 --- /dev/null +++ b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts @@ -0,0 +1,62 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import {getSortOrder} from '../../../../client/shared/sortOrder' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const moveReflectTemplatePrompt: MutationResolvers['moveReflectTemplatePrompt'] = async ( + _source, + {promptId, sortOrder}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // RESOLUTION + const {teamId} = prompt + + const oldPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const fromIdx = oldPrompts.findIndex((p) => p.id === promptId) + + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + sortOrder, + updatedAt: now + }) + .run() + ]) + dataLoader.clearAll('reflectPrompts') + const newPrompts = await dataLoader.get('reflectPromptsByTemplateId').load(prompt.templateId) + const pgPrompts = await dataLoader.get('_pgreflectPromptsByTemplateId').load(prompt.templateId) + const toIdx = newPrompts.findIndex((p) => p.id === promptId) + const pgSortOrder = getSortOrder(pgPrompts, fromIdx, toIdx) + await pg + .updateTable('ReflectPrompt') + .set({sortOrder: pgSortOrder}) + .where('id', '=', promptId) + .execute() + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'MoveReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default moveReflectTemplatePrompt diff --git a/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts new file mode 100644 index 00000000000..5fc36d0fb48 --- /dev/null +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts @@ -0,0 +1,59 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const reflectTemplatePromptUpdateDescription: MutationResolvers['reflectTemplatePromptUpdateDescription'] = + async (_source, {promptId, description}, {authToken, dataLoader, socketId: mutatorId}) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId} = prompt + const normalizedDescription = description.trim().slice(0, 256) || '' + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + description: normalizedDescription, + updatedAt: now + }) + .run(), + pg + .updateTable('ReflectPrompt') + .set({description: normalizedDescription}) + .where('id', '=', promptId) + .execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId} + publish( + SubscriptionChannel.TEAM, + teamId, + 'ReflectTemplatePromptUpdateDescriptionPayload', + data, + subOptions + ) + return data + } + +export default reflectTemplatePromptUpdateDescription diff --git a/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts new file mode 100644 index 00000000000..f6de5ffc8f2 --- /dev/null +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts @@ -0,0 +1,55 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const reflectTemplatePromptUpdateGroupColor: MutationResolvers['reflectTemplatePromptUpdateGroupColor'] = + async (_source, {promptId, groupColor}, {authToken, dataLoader, socketId: mutatorId}) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const viewerId = getUserId(authToken) + + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId} = prompt + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + groupColor, + updatedAt: now + }) + .run(), + pg.updateTable('ReflectPrompt').set({groupColor}).where('id', '=', promptId).execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId} + publish( + SubscriptionChannel.TEAM, + teamId, + 'ReflectTemplatePromptUpdateGroupColorPayload', + data, + subOptions + ) + return data + } + +export default reflectTemplatePromptUpdateGroupColor diff --git a/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts new file mode 100644 index 00000000000..c74b0b48b0a --- /dev/null +++ b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts @@ -0,0 +1,58 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const removeReflectTemplatePrompt: MutationResolvers['removeReflectTemplatePrompt'] = async ( + _source, + {promptId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId, templateId} = prompt + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const activePrompts = prompts.filter((p) => !p.removedAt) + const promptCount = activePrompts.length + + if (promptCount <= 1) { + return standardError(new Error('No prompts remain'), {userId: viewerId}) + } + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + removedAt: now, + updatedAt: now + }) + .run(), + pg.updateTable('ReflectPrompt').set({removedAt: now}).where('id', '=', promptId).execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId, templateId} + publish(SubscriptionChannel.TEAM, teamId, 'RemoveReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default removeReflectTemplatePrompt diff --git a/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts new file mode 100644 index 00000000000..426901b79e6 --- /dev/null +++ b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts @@ -0,0 +1,63 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const renameReflectTemplatePrompt: MutationResolvers['renameReflectTemplatePrompt'] = async ( + _source, + {promptId, question}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const now = new Date() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const prompt = await dataLoader.get('reflectPrompts').load(promptId) + const viewerId = getUserId(authToken) + + // AUTH + if (!prompt || prompt.removedAt) { + return standardError(new Error('Prompt not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, prompt.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId, templateId} = prompt + const trimmedQuestion = question.trim().slice(0, 100) + const normalizedQuestion = trimmedQuestion || 'Unnamed Prompt' + + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(templateId) + const allPrompts = prompts.filter(({removedAt}) => !removedAt) + if (allPrompts.find((prompt) => prompt.question === normalizedQuestion)) { + return standardError(new Error('Duplicate question template'), {userId: viewerId}) + } + + // RESOLUTION + await Promise.all([ + r + .table('ReflectPrompt') + .get(promptId) + .update({ + question: normalizedQuestion, + updatedAt: now + }) + .run(), + pg + .updateTable('ReflectPrompt') + .set({question: normalizedQuestion}) + .where('id', '=', promptId) + .execute() + ]) + dataLoader.clearAll('reflectPrompts') + const data = {promptId} + publish(SubscriptionChannel.TEAM, teamId, 'RenameReflectTemplatePromptPayload', data, subOptions) + return data +} + +export default renameReflectTemplatePrompt diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index 0a58cdfde64..1ff66c7adb6 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -41,7 +41,7 @@ type Mutation { """ Add a new template full of prompts """ - addReflectTemplatePrompt(templateId: ID!): AddReflectTemplatePromptPayload + addReflectTemplatePrompt(templateId: ID!): AddReflectTemplatePromptPayload! addSlackAuth(code: ID!, teamId: ID!): AddSlackAuthPayload! addGitHubAuth(code: ID!, teamId: ID!): AddGitHubAuthPayload! diff --git a/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..59482f4f743 --- /dev/null +++ b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {AddReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type AddReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const AddReflectTemplatePromptPayload: AddReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default AddReflectTemplatePromptPayload diff --git a/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..87096f79f55 --- /dev/null +++ b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {MoveReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type MoveReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const MoveReflectTemplatePromptPayload: MoveReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default MoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index 9943e8fac56..fb5bad8671e 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -1,5 +1,4 @@ import MeetingRetrospective from '../../../database/types/MeetingRetrospective' -import RetrospectivePrompt from '../../../database/types/RetrospectivePrompt' import {ReflectPhaseResolvers} from '../resolverTypes' const ReflectPhase: ReflectPhaseResolvers = { @@ -15,7 +14,7 @@ const ReflectPhase: ReflectPhaseResolvers = { // only show prompts that were created before the meeting and // either have not been removed or they were removed after the meeting was created return prompts.filter( - (prompt: RetrospectivePrompt) => + (prompt) => prompt.createdAt < meeting.createdAt && (!prompt.removedAt || meeting.createdAt < prompt.removedAt) ) diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts new file mode 100644 index 00000000000..ab49737cd6e --- /dev/null +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts @@ -0,0 +1,16 @@ +import {ReflectTemplatePromptUpdateDescriptionPayloadResolvers} from '../resolverTypes' + +export type ReflectTemplatePromptUpdateDescriptionPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const ReflectTemplatePromptUpdateDescriptionPayload: ReflectTemplatePromptUpdateDescriptionPayloadResolvers = + { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } + } + +export default ReflectTemplatePromptUpdateDescriptionPayload diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts new file mode 100644 index 00000000000..c964750561f --- /dev/null +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts @@ -0,0 +1,16 @@ +import {ReflectTemplatePromptUpdateGroupColorPayloadResolvers} from '../resolverTypes' + +export type ReflectTemplatePromptUpdateGroupColorPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const ReflectTemplatePromptUpdateGroupColorPayload: ReflectTemplatePromptUpdateGroupColorPayloadResolvers = + { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } + } + +export default ReflectTemplatePromptUpdateGroupColorPayload diff --git a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..8e81e655db0 --- /dev/null +++ b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts @@ -0,0 +1,22 @@ +import {RemoveReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type RemoveReflectTemplatePromptPayloadSource = + | { + promptId: string + templateId: string + } + | {error: {message: string}} + +const RemoveReflectTemplatePromptPayload: RemoveReflectTemplatePromptPayloadResolvers = { + reflectTemplate: (source, _args, {dataLoader}) => { + return 'templateId' in source + ? dataLoader.get('meetingTemplates').loadNonNull(source.templateId) + : null + }, + + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default RemoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts new file mode 100644 index 00000000000..980ad0ac390 --- /dev/null +++ b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts @@ -0,0 +1,15 @@ +import {RenameReflectTemplatePromptPayloadResolvers} from '../resolverTypes' + +export type RenameReflectTemplatePromptPayloadSource = + | { + promptId: string + } + | {error: {message: string}} + +const RenameReflectTemplatePromptPayload: RenameReflectTemplatePromptPayloadResolvers = { + prompt: (source, _args, {dataLoader}) => { + return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + } +} + +export default RenameReflectTemplatePromptPayload diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index ab88c2e03ba..918463c1dae 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -7,7 +7,6 @@ import addOrg from './mutations/addOrg' import addPokerTemplateDimension from './mutations/addPokerTemplateDimension' import addPokerTemplateScale from './mutations/addPokerTemplateScale' import addPokerTemplateScaleValue from './mutations/addPokerTemplateScaleValue' -import addReflectTemplatePrompt from './mutations/addReflectTemplatePrompt' import addTeam from './mutations/addTeam' import archiveOrganization from './mutations/archiveOrganization' import archiveTeam from './mutations/archiveTeam' @@ -41,7 +40,6 @@ import inviteToTeam from './mutations/inviteToTeam' import joinMeeting from './mutations/joinMeeting' import movePokerTemplateDimension from './mutations/movePokerTemplateDimension' import movePokerTemplateScaleValue from './mutations/movePokerTemplateScaleValue' -import moveReflectTemplatePrompt from './mutations/moveReflectTemplatePrompt' import moveTeamToOrg from './mutations/moveTeamToOrg' import navigateMeeting from './mutations/navigateMeeting' import newMeetingCheckIn from './mutations/newMeetingCheckIn' @@ -57,8 +55,6 @@ import pokerTemplateDimensionUpdateDescription from './mutations/pokerTemplateDi import promoteNewMeetingFacilitator from './mutations/promoteNewMeetingFacilitator' import promoteToTeamLead from './mutations/promoteToTeamLead' import pushInvitation from './mutations/pushInvitation' -import reflectTemplatePromptUpdateDescription from './mutations/reflectTemplatePromptUpdateDescription' -import reflectTemplatePromptUpdateGroupColor from './mutations/reflectTemplatePromptUpdateGroupColor' import removeAtlassianAuth from './mutations/removeAtlassianAuth' import removeGitHubAuth from './mutations/removeGitHubAuth' import removeIntegrationProvider from './mutations/removeIntegrationProvider' @@ -67,7 +63,6 @@ import removePokerTemplateDimension from './mutations/removePokerTemplateDimensi import removePokerTemplateScale from './mutations/removePokerTemplateScale' import removePokerTemplateScaleValue from './mutations/removePokerTemplateScaleValue' import removeReflectTemplate from './mutations/removeReflectTemplate' -import removeReflectTemplatePrompt from './mutations/removeReflectTemplatePrompt' import removeReflection from './mutations/removeReflection' import removeSlackAuth from './mutations/removeSlackAuth' import removeTeamMember from './mutations/removeTeamMember' @@ -75,7 +70,6 @@ import renameMeeting from './mutations/renameMeeting' import renameMeetingTemplate from './mutations/renameMeetingTemplate' import renamePokerTemplateDimension from './mutations/renamePokerTemplateDimension' import renamePokerTemplateScale from './mutations/renamePokerTemplateScale' -import renameReflectTemplatePrompt from './mutations/renameReflectTemplatePrompt' import resetPassword from './mutations/resetPassword' import resetRetroMeetingToGroupStage from './mutations/resetRetroMeetingToGroupStage' import selectTemplate from './mutations/selectTemplate' @@ -114,7 +108,6 @@ export default new GraphQLObjectType({ addPokerTemplateDimension, addPokerTemplateScale, addPokerTemplateScaleValue, - addReflectTemplatePrompt, addGitHubAuth, addOrg, addTeam, @@ -148,7 +141,6 @@ export default new GraphQLObjectType({ invalidateSessions, inviteToTeam, movePokerTemplateDimension, - moveReflectTemplatePrompt, moveTeamToOrg, navigateMeeting, newMeetingCheckIn, @@ -157,18 +149,14 @@ export default new GraphQLObjectType({ pushInvitation, promoteNewMeetingFacilitator, promoteToTeamLead, - reflectTemplatePromptUpdateDescription, pokerTemplateDimensionUpdateDescription, - reflectTemplatePromptUpdateGroupColor, removeAtlassianAuth, removeGitHubAuth, removeOrgUser, removeReflectTemplate, - removeReflectTemplatePrompt, removePokerTemplateDimension, renameMeeting, renameMeetingTemplate, - renameReflectTemplatePrompt, renamePokerTemplateDimension, renamePokerTemplateScale, removePokerTemplateScale, diff --git a/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts b/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts deleted file mode 100644 index 9061c731a8b..00000000000 --- a/packages/server/graphql/types/AddReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const AddReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'AddReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default AddReflectTemplatePromptPayload diff --git a/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts b/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts deleted file mode 100644 index c6d4ea310e7..00000000000 --- a/packages/server/graphql/types/MoveReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const MoveReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'MoveReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default MoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts b/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts deleted file mode 100644 index 4e27cbaa23c..00000000000 --- a/packages/server/graphql/types/ReflectTemplatePromptUpdateDescriptionPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const ReflectTemplatePromptUpdateDescriptionPayload = new GraphQLObjectType({ - name: 'ReflectTemplatePromptUpdateDescriptionPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default ReflectTemplatePromptUpdateDescriptionPayload diff --git a/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts b/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts deleted file mode 100644 index 6efd27f0c8c..00000000000 --- a/packages/server/graphql/types/ReflectTemplatePromptUpdateGroupColorPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const ReflectTemplatePromptUpdateGroupColorPayload = new GraphQLObjectType({ - name: 'ReflectTemplatePromptUpdateGroupColorPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default ReflectTemplatePromptUpdateGroupColorPayload diff --git a/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts deleted file mode 100644 index 8c66ecd729e..00000000000 --- a/packages/server/graphql/types/RemoveReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import ReflectTemplate from './ReflectTemplate' -import StandardMutationError from './StandardMutationError' - -const RemoveReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'RemoveReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - reflectTemplate: { - type: ReflectTemplate, - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - if (!templateId) return null - return dataLoader.get('meetingTemplates').load(templateId) - } - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default RemoveReflectTemplatePromptPayload diff --git a/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts b/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts deleted file mode 100644 index 0fe41ac3764..00000000000 --- a/packages/server/graphql/types/RenameReflectTemplatePromptPayload.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import ReflectPrompt from './ReflectPrompt' -import StandardMutationError from './StandardMutationError' - -const RenameReflectTemplatePromptPayload = new GraphQLObjectType({ - name: 'RenameReflectTemplatePromptPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - prompt: { - type: ReflectPrompt, - resolve: ({promptId}, _args: unknown, {dataLoader}) => { - if (!promptId) return null - return dataLoader.get('reflectPrompts').load(promptId) - } - } - }) -}) - -export default RenameReflectTemplatePromptPayload diff --git a/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts new file mode 100644 index 00000000000..425985da005 --- /dev/null +++ b/packages/server/postgres/migrations/1725655687704_ReflectPrompt-phase1.ts @@ -0,0 +1,56 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "ReflectPrompt" ( + "id" VARCHAR(100) PRIMARY KEY, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "removedAt" TIMESTAMP WITH TIME ZONE, + "description" VARCHAR(256) NOT NULL, + "groupColor" VARCHAR(9) NOT NULL, + "sortOrder" VARCHAR(64) NOT NULL COLLATE "C", + "question" VARCHAR(100) NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "templateId" VARCHAR(100) NOT NULL, + "parentPromptId" VARCHAR(100), + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_templateId" + FOREIGN KEY("templateId") + REFERENCES "MeetingTemplate"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_teamId" ON "ReflectPrompt"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_templateId" ON "ReflectPrompt"("templateId"); + CREATE INDEX IF NOT EXISTS "idx_ReflectPrompt_parentPromptId" ON "ReflectPrompt"("templateId"); + CREATE OR REPLACE TRIGGER "update_MeetingTemplate_updatedAt_from_ReflectPrompt" + AFTER INSERT OR UPDATE OR DELETE ON "ReflectPrompt" + FOR EACH ROW + EXECUTE FUNCTION "set_MeetingTemplate_updatedAt"(); + END $$; +`) + // TODO add constraint parentPromptId constraint + // CONSTRAINT "fk_parentPromptId" + // FOREIGN KEY("parentPromptId") + // REFERENCES "MeetingTemplate"("id") + // ON DELETE CASCADE + + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "ReflectPrompt"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/queries/insertMeetingTemplate.ts b/packages/server/postgres/queries/insertMeetingTemplate.ts deleted file mode 100644 index 78e06f2b53b..00000000000 --- a/packages/server/postgres/queries/insertMeetingTemplate.ts +++ /dev/null @@ -1,39 +0,0 @@ -import MeetingTemplate from '../../database/types/MeetingTemplate' -import getPg from '../getPg' - -const insertMeetingTemplate = async (meetingTemplate: MeetingTemplate) => { - const pg = getPg() - const { - id, - name, - teamId, - orgId, - parentTemplateId, - type, - scope, - lastUsedAt, - isStarter, - isFree, - mainCategory, - illustrationUrl - } = meetingTemplate - await pg.query( - `INSERT INTO "MeetingTemplate" (id, name, "teamId", "orgId", "parentTemplateId", type, scope, "lastUsedAt", "isStarter", "isFree", "mainCategory", "illustrationUrl") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, - [ - id, - name, - teamId, - orgId, - parentTemplateId, - type, - scope, - lastUsedAt, - isStarter, - isFree, - mainCategory, - illustrationUrl - ] - ) -} - -export default insertMeetingTemplate diff --git a/packages/server/postgres/queries/removeMeetingTemplate.ts b/packages/server/postgres/queries/removeMeetingTemplate.ts deleted file mode 100644 index 8793f726ff6..00000000000 --- a/packages/server/postgres/queries/removeMeetingTemplate.ts +++ /dev/null @@ -1,8 +0,0 @@ -import getPg from '../getPg' - -const removeMeetingTemplate = async (templateId: string) => { - const pg = getPg() - await pg.query(`UPDATE "MeetingTemplate" SET "isActive" = FALSE WHERE id = $1;`, [templateId]) -} - -export default removeMeetingTemplate diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index f97275eb80b..affc2e06e00 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -228,3 +228,5 @@ export const selectComments = () => 'threadSortOrder' ]) .select(({fn}) => [fn('to_json', ['reactjis']).as('reactjis')]) + +export const selectReflectPrompts = () => getKysely().selectFrom('ReflectPrompt').selectAll() diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 48fb2419996..affb35215d4 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -9,6 +9,7 @@ import { selectComments, selectMeetingSettings, selectOrganizations, + selectReflectPrompts, selectRetroReflections, selectSlackAuths, selectSlackNotifications, @@ -56,3 +57,4 @@ export type SlackAuth = ExtractTypeFromQueryBuilderSelect export type Comment = ExtractTypeFromQueryBuilderSelect +export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect