Skip to content

Commit

Permalink
Merge pull request #1148 from givepraise/add/direct-praise-quantifica…
Browse files Browse the repository at this point in the history
…tion

Add/direct praise quantification
  • Loading branch information
kristoferlund authored Dec 15, 2023
2 parents 0517f80 + d8ef1e0 commit e6727f2
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 24 deletions.
2 changes: 2 additions & 0 deletions packages/api-types/out/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ export interface components {
sourceName: string;
receiverIds: string[];
giver: components['schemas']['UserAccount'];
score?: number;
};
PraiseForwardInputDto: {
/** @example for making edits in the welcome text */
Expand All @@ -787,6 +788,7 @@ export interface components {
receiverIds: string[];
giver: components['schemas']['UserAccount'];
forwarder: components['schemas']['UserAccount'];
score?: number;
};
CreateUserAccountInputDto: {
/** @example 098098098098098 */
Expand Down
2 changes: 1 addition & 1 deletion packages/api/openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SettingGroup } from '../../settings/enums/setting-group.enum';
import { SettingModel } from '../schemas/settings/23_settings.schema';

const settings = [
{
key: 'DISCORD_BOT_DIRECT_PRAISE_QUANTIFICATION_ENABLED',
value: false,
defaultValue: false,
type: 'Boolean',
periodOverridable: false,
label: 'Direct praise quantification',
description:
'Enabling this will allow the praise giver to quantify their praise directly at the time of praising. Enabling this will disable and bypass the regular praise quantification flow.',
group: SettingGroup.DISCORD,
subgroup: 1,
},
];

const up = async (): Promise<void> => {
const settingUpdates = settings.map((s) => ({
updateOne: {
filter: { key: s.key },
update: { $setOnInsert: { ...s } }, // Insert setting if not found, otherwise continue
upsert: true,
},
})) as any;

await SettingModel.bulkWrite(settingUpdates);
};

const down = async (): Promise<void> => {
const allKeys = settings.map((s) => s.key);
await SettingModel.deleteMany({ key: { $in: allKeys } });
};

export { up, down };
6 changes: 5 additions & 1 deletion packages/api/src/praise/dto/praise-create-input.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserAccount } from '../../useraccounts/schemas/useraccounts.schema';
import { ApiProperty, PickType } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';
import { Praise } from '../schemas/praise.schema';

export class PraiseCreateInputDto extends PickType(Praise, [
Expand All @@ -21,4 +21,8 @@ export class PraiseCreateInputDto extends PickType(Praise, [
@Type(() => UserAccount)
@IsNotEmpty()
giver: UserAccount;

@ApiProperty({ required: false })
@IsOptional()
score: number;
}
6 changes: 5 additions & 1 deletion packages/api/src/praise/dto/praise-forward-input.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserAccount } from '../../useraccounts/schemas/useraccounts.schema';
import { ApiProperty, PickType } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, ValidateNested } from 'class-validator';
import { IsNotEmpty, IsOptional, ValidateNested } from 'class-validator';
import { PraiseCreateInputDto } from './praise-create-input.dto';

export class PraiseForwardInputDto extends PickType(PraiseCreateInputDto, [
Expand All @@ -19,4 +19,8 @@ export class PraiseForwardInputDto extends PickType(PraiseCreateInputDto, [
@Type(() => UserAccount)
@IsNotEmpty()
forwarder: UserAccount;

@ApiProperty({ required: false })
@IsOptional()
score: number;
}
37 changes: 34 additions & 3 deletions packages/api/src/praise/services/praise.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,15 @@ export class PraiseService {
data: PraiseCreateInputDto | PraiseForwardInputDto,
): Promise<Praise[]> => {
let forwarder: UserAccount | undefined;
const { giver, receiverIds, reason, reasonRaw, sourceId, sourceName } =
data;
const {
giver,
receiverIds,
reason,
reasonRaw,
sourceId,
sourceName,
score,
} = data;
if ('forwarder' in data) {
const { forwarder: forwarderFromDto } = data as PraiseForwardInputDto;
forwarder = forwarderFromDto;
Expand Down Expand Up @@ -274,6 +281,16 @@ export class PraiseService {
})
.lean();

const directQuantificationEnanbledSetting =
(await this.settingsService.findOneByKey(
'DISCORD_BOT_DIRECT_PRAISE_QUANTIFICATION_ENABLED',
)) as Setting;

const directQuantificationEnabled =
directQuantificationEnanbledSetting?.value === 'true' &&
score &&
score > 0;

const newPraise = await this.praiseModel.insertMany(
receivers.map((receiver) => ({
reason,
Expand All @@ -283,13 +300,27 @@ export class PraiseService {
sourceId,
sourceName,
receiver: receiver._id,
score: directQuantificationEnabled ? score : undefined,
})),
);

const findPraisePromises = newPraise.map((praise) =>
this.findOneById(praise._id),
);

return Promise.all(findPraisePromises);
const createdPraise = Promise.all(findPraisePromises);

if (directQuantificationEnabled) {
await this.quantificationModel.insertMany(
newPraise.map((praise) => ({
score: data.score,
scoreRealized: data.score,
praise: praise._id,
quantifier: giverAccount.user,
})),
);
}

return createdPraise;
};
}
80 changes: 69 additions & 11 deletions packages/discord-bot/src/handlers/praise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
GuildMember,
ActionRowBuilder,
ButtonBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
} from 'discord.js';
import { parseReceivers } from '../utils/parseReceivers';

Expand All @@ -15,6 +17,7 @@ import { getUserAccount } from '../utils/getUserAccount';
import { logger } from '../utils/logger';
import { sendActivationMessage } from '../utils/sendActivationMessage';
import { givePraise } from '../utils/givePraise';
import { getSetting } from '../utils/settingsUtil';

/**
* Execute command /praise
Expand All @@ -28,7 +31,7 @@ export const praiseHandler: CommandHandler = async (
host,
responseUrl
) => {
if (!responseUrl) return;
if (!responseUrl || !interaction) return;

const { guild, member } = interaction;

Expand Down Expand Up @@ -160,17 +163,72 @@ export const praiseHandler: CommandHandler = async (
);
}
} else {
await givePraise(
interaction,
guild,
member as GuildMember,
giverAccount,
parsedReceivers,
receiverOptions,
reason,
host,
responseUrl
const directQuantificationEnanbled = (await getSetting(
'DISCORD_BOT_DIRECT_PRAISE_QUANTIFICATION_ENABLED'
)) as boolean;

// If direct quantification is disabled, give praise directly
// This is the default behavior
if (!directQuantificationEnanbled) {
await givePraise(
interaction,
guild,
member as GuildMember,
giverAccount,
parsedReceivers,
receiverOptions,
reason,
host,
responseUrl
);
return;
}

// If direct quantification is enabled, allow user to select a score from a dropdown
const allowedScores = (await getSetting(
'PRAISE_QUANTIFY_ALLOWED_VALUES'
)) as number[];

const select = new StringSelectMenuBuilder()
.setCustomId('score')
.setPlaceholder('Select an impact score!')
.addOptions(
allowedScores.map((score) =>
new StringSelectMenuOptionBuilder()
.setLabel(score.toString())
.setValue(score.toString())
)
);

const row = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
select
);

await interaction.followUp({
content: 'Select an impact score!',
components: [row],
});

const collector = interaction.channel?.createMessageComponentCollector({
componentType: ComponentType.StringSelect,
time: 15000,
});

collector?.on('collect', async (menuInteraction) => {
const score = Number(menuInteraction.values[0]);
await givePraise(
interaction,
guild,
member as GuildMember,
giverAccount,
parsedReceivers,
receiverOptions,
reason,
host,
responseUrl,
score
);
});
}
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
5 changes: 4 additions & 1 deletion packages/discord-bot/src/utils/createPraise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ interface PraiseCreateInputDto {
receiverIds: string[];
sourceId: string;
sourceName: string;
score?: number;
}

export const createPraise = async (
interaction: ChatInputCommandInteraction,
giverAccount: UserAccount,
receiverAccounts: UserAccount[],
reason: string,
host: string
host: string,
score?: number
): Promise<Praise[]> => {
const { channel, guild } = interaction;
if (!channel || !guild || channel.type === ChannelType.DM) return [];
Expand All @@ -52,6 +54,7 @@ export const createPraise = async (
sourceName: `DISCORD:${encodeURIComponent(guild.name)}:${encodeURIComponent(
channelName
)}`,
score: score,
};

const response = await apiPost<Praise[], PraiseCreateInputDto>(
Expand Down
6 changes: 4 additions & 2 deletions packages/discord-bot/src/utils/givePraise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const givePraise = async (
receiverOptions: string,
reason: string,
host: string,
responseUrl: string
responseUrl: string,
score?: number
): Promise<void> => {
if (
!parsedReceivers.validReceiverIds ||
Expand Down Expand Up @@ -88,7 +89,8 @@ export const givePraise = async (
giverAccount,
receivers.map((receiver) => receiver.userAccount),
reason,
host
host,
score
);
} else if (warnSelfPraise) {
await ephemeralWarning(interaction, 'SELF_PRAISE_WARNING', host);
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const Button = ({
const disabledModifier =
'disabled:cursor-default disabled:bg-themecolor-3/50 disabled:text-white/50';

const defaultClass = `px-4 py-2 font-bold text-white rounded-md bg-themecolor-3 hover:bg-themecolor-4 ${disabledModifier}`;
const outlineClass = `border-2 border-themecolor-3 px-4 py-2 font-bold text-white rounded-md bg-themecolor-3 hover:bg-themecolor-4 ${disabledModifier}`;
const defaultClass = `h-10 whitespace-nowrap px-4 py-2 font-bold text-white rounded-md bg-themecolor-3 hover:bg-themecolor-4 ${disabledModifier}`;
const outlineClass = `h-10 whitespace-nowrap border-2 border-themecolor-3 px-4 py-2 font-bold text-white rounded-md bg-themecolor-3 hover:bg-themecolor-4 ${disabledModifier}`;
const roundClass = `flex items-center justify-center rounded-full hover:bg-warm-gray-300 w-7 h-7 dark:text-white dark:hover:bg-slate-800 ${disabledModifier}`;

let variantClass = defaultClass;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CloseButton } from './CloseButton';
import { PeriodDateForm } from './PeriodDateForm';
import { PeriodNameForm } from './PeriodNameForm';
import { AssignButton } from './AssignButton';
import { SingleSetting } from '../../../model/settings/settings';

export const PeriodDetailsHead = (): JSX.Element | null => {
const { periodId } = useParams<PeriodPageParams>();
Expand All @@ -22,6 +23,9 @@ export const PeriodDetailsHead = (): JSX.Element | null => {
useLoadSinglePeriodDetails(periodId); // Fetch additional period details
const period = useRecoilValue(SinglePeriod(periodId));
const isAdmin = useRecoilValue(HasRole(ROLE_ADMIN));
const directQuantEnabled = useRecoilValue(
SingleSetting('DISCORD_BOT_DIRECT_PRAISE_QUANTIFICATION_ENABLED')
);

if (!period || !allPeriods) return null;

Expand Down Expand Up @@ -63,13 +67,22 @@ export const PeriodDetailsHead = (): JSX.Element | null => {
<div>Number of praise: {period.numberOfPraise}</div>
<div className="mt-5">
{period.status === 'OPEN' || period.status === 'QUANTIFY' ? (
<div className="flex justify-between gap-4">
<div className="flex items-center justify-between gap-10">
{period.status === 'OPEN' &&
period.receivers &&
period?.receivers.length > 0 &&
hasPeriodEnded(period) ? (
<AssignButton />
directQuantEnabled?.valueRealized ? (
<div className="max-w-md">
Direct quantification is enabled for this community which
means each praise is quantified by the praise giver at
praise time. Regular quantifications are disabled.
</div>
) : (
<AssignButton />
)
) : null}

<CloseButton />
</div>
) : null}
Expand Down

0 comments on commit e6727f2

Please sign in to comment.