Skip to content
Merged
10 changes: 7 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ And an [Elastic Search](https://www.elastic.co/guide/en/elasticsearch/reference/

## Remote Environment (Codespaces / Gitpod)

There are shortcut buttons in the README to jumpstart running from a remote environment. When running from these remote environments, consider the following:
There are shortcut buttons in the README to jump start running from a remote environment. When running from these remote environments, consider the following:

- They require at least 8 GB of ram to run the databases
- If you have a slow remote machine, then the tests may fail when you run them locally if you run them all at the same time from the root directory.
Expand All @@ -38,10 +38,12 @@ There are shortcut buttons in the README to jumpstart running from a remote envi
- This is only for packages that don't make any calls to the databases. If you try to run the tests on the whole project without databases setup the tests will fail

## UI
We use storybook for developing our components and pages. It offers an isolated enviroment with a powerful toolset to empower frontend development.

We use storybook for developing our components and pages. It offers an isolated environment with a powerful tool set to empower frontend development.

### Design
For more information on designing Answer Overflow UI's, check out our [Design Guidlines](./DESIGN_GUIDELINES.md).

For more information on designing Answer Overflow UI's, check out our [Design Guidelines](./DESIGN_GUIDELINES.md).

### Gitpod

Expand All @@ -56,6 +58,8 @@ For more information on designing Answer Overflow UI's, check out our [Design Gu

There is a workspace file called answeroverflow.code-workspace, VSCode lets you open this folder as that workspace and it is recommended that you do your development work inside of this workspace as it will configure all of the settings for you

! The workspace is set to hide all "useless" files (i.e node_modules) if you for some reason need to access them, comment out the line hiding them in the workspace file !

### Get it running

Copy the .env.example file in the root directory and create a new file titled .env
Expand Down
36 changes: 30 additions & 6 deletions answeroverflow.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@
]
],
// "tailwindCSS.experimental.configFile": "./packages/config/tailwind/index.js",
// Disable these to view "useless" files
"files.exclude": {
// "apps/": true, // disable to make the root view cleaner, left on to allow for edits w/out leaving workspace
// "packages/": true, // disable to make the root view cleaner, left on to allow for edits w/out leaving workspace
// "node_modules/": true,
// "dist/": true,
// "coverage/": true,
"**/node_modules": true,
"**/coverage/": true,
"**/.next": true,
"**/.turbo": true,
"**/dist": true,
},
// "typescript.tsserver.experimental.enableProjectDiagnostics": true,
"vitest.enable": true,
Expand Down Expand Up @@ -148,15 +149,26 @@
],
"jest.autoRun": "off",
"cSpell.words": [
"Anson",
"Bitfield",
"Bools",
"Codespaces",
"dbaeumer",
"devcontainer",
"devcontainers",
"extensionpack",
"nextjs",
"Nextra",
"orta",
"reacord",
"Rhys",
"superjson",
"tailwindcss",
"trpc",
"Turborepo",
"twoslash",
"unauthed",
"undelete",
"upsert",
"upserting"
],
Expand All @@ -165,7 +177,19 @@
"recommendations": [
"vscode-icons-team.vscode-icons",
"ms-vscode-remote.vscode-remote-extensionpack",
"orta.vscode-twoslash-queries"
"orta.vscode-twoslash-queries",
"dbaeumer.vscode-eslint",
"GitHub.copilot",
"eamodio.gitlens",
"Prisma.prisma",
"aaron-bond.better-comments",
"ZixuanChen.vitest-explorer",
"EditorConfig.EditorConfig",
"bradlc.vscode-tailwindcss",
"DavidAnson.vscode-markdownlint",
"Orta.vscode-jest",
"unifiedjs.vscode-mdx",
"usernamehw.errorlens"
]
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export class OpenManageAccountMenuCommand extends Command {
}
const menu = (
<ManageAccountMenu
initalSettings={userServerSettings}
initalIsGloballyIgnored={isIgnoredAccount}
initialSettings={userServerSettings}
initialIsGloballyIgnored={isIgnoredAccount}
/>
);
ephemeralReply(container.reacord, menu, interaction);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ApplyOptions } from "@sapphire/decorators";
import { Command, container, type ChatInputCommand } from "@sapphire/framework";
import { callAPI, callWithAllowedErrors, ephemeralStatusHandler } from "~discord-bot/utils/trpc";
import { SlashCommandBuilder, type ChatInputCommandInteraction } from "discord.js";
import React from "react";
import { ephemeralReply } from "~discord-bot/utils/utils";
import { getDefaultServerWithFlags } from "@answeroverflow/db";
import { createMemberCtx } from "~discord-bot/utils/context";

import { guildTextChannelOnlyInteraction } from "~discord-bot/utils/conditions";
import { ServerSettingsMenu } from "~discord-bot/components/server-settings-menu";
import { toAOServer } from "~discord-bot/utils/conversions";
import type { ServerAll } from "@answeroverflow/api";

@ApplyOptions<Command.Options>({
name: "server-settings",
description: "Manage your server's Answer Overflow settings",
runIn: ["GUILD_ANY"],
})
export class OpenServerSettingsMenu extends Command {
public override registerApplicationCommands(registry: ChatInputCommand.Registry) {
registry.registerChatInputCommand(
new SlashCommandBuilder()
.setName(this.name)
.setDescription(this.description)
.setDMPermission(false)
);
}

public override async chatInputRun(interaction: ChatInputCommandInteraction) {
await guildTextChannelOnlyInteraction(interaction, async ({ guild, member }) => {
await callAPI({
async apiCall(router) {
const server = await callWithAllowedErrors({
call: () => router.servers.byId(guild.id),
allowedErrors: "NOT_FOUND",
});
return server;
},
getCtx: () => createMemberCtx(member),
Error: (error) => ephemeralStatusHandler(interaction, error.message),
Ok(server) {
if (!server) {
server = getDefaultServerWithFlags(toAOServer(guild));
}
const menu = <ServerSettingsMenu server={server as ServerAll} />;
ephemeralReply(container.reacord, menu, interaction);
},
});
});
}
}
6 changes: 6 additions & 0 deletions apps/discord-bot/src/components/instructions-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Embed } from "@answeroverflow/reacord";
import { ANSWER_OVERFLOW_BLUE_AS_INT } from "~discord-bot/utils/constants";
import React from "react";
export const InstructionsContainer = ({ children }: { children: React.ReactNode }) => (
<Embed color={ANSWER_OVERFLOW_BLUE_AS_INT}>{children}</Embed>
);
22 changes: 22 additions & 0 deletions apps/discord-bot/src/components/instructions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { Spacer } from "./spacer";

export type MenuInstruction = {
title: string;
instructions: string;
enabled: boolean;
};

export const EmbedMenuInstruction = ({ instructions }: { instructions: MenuInstruction[] }) => (
<React.Fragment>
{instructions.map(
({ title, instructions, enabled }) =>
enabled && (
<React.Fragment key={title}>
**{title}** - {instructions}
<Spacer count={2} />
</React.Fragment>
)
)}
</React.Fragment>
);
117 changes: 59 additions & 58 deletions apps/discord-bot/src/components/manage-account-menu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { reply } from "~discord-bot/test/reacord-utils";
import { reply, toggleButtonTest } from "~discord-bot/test/reacord-utils";
import React from "react";
import {
createDiscordAccount,
Expand Down Expand Up @@ -52,16 +52,16 @@ describe("Manage Account Menu", () => {
it("should enable consent", async () => {
const message = await reply(
reacord,
<ManageAccountMenu initalSettings={defaultSettings} initalIsGloballyIgnored={false} />
<ManageAccountMenu initialSettings={defaultSettings} initialIsGloballyIgnored={false} />
);
const enableIndexingButton = message!.findButtonByLabel(GRANT_CONSENT_LABEL, reacord);
expect(enableIndexingButton).toBeDefined();
await enableIndexingButton!.click(textChannel, members.guildMemberOwner);
// Used to verify no errors were thrown
expect(reacord.messages).toHaveLength(1);
expect(message!.hasButton(GRANT_CONSENT_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(REVOKE_CONSENT_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: GRANT_CONSENT_LABEL,
postClickLabel: REVOKE_CONSENT_LABEL,
message: message!,
reacord,
channel: textChannel,
});
});
it("should disable consent", async () => {
await createDiscordAccount(toAODiscordAccount(members.guildMemberOwner.user));
Expand All @@ -77,16 +77,16 @@ describe("Manage Account Menu", () => {

const message = await reply(
reacord,
<ManageAccountMenu initalSettings={initialSettings} initalIsGloballyIgnored={false} />
<ManageAccountMenu initialSettings={initialSettings} initialIsGloballyIgnored={false} />
);
const disableIndexingButton = message!.findButtonByLabel(REVOKE_CONSENT_LABEL, reacord);
expect(disableIndexingButton).toBeDefined();
await disableIndexingButton!.click(textChannel, members.guildMemberOwner);
// Used to verify no errors were thrown
expect(reacord.messages).toHaveLength(1);
expect(message!.hasButton(REVOKE_CONSENT_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(GRANT_CONSENT_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: REVOKE_CONSENT_LABEL,
postClickLabel: GRANT_CONSENT_LABEL,
message: message!,
reacord,
channel: textChannel,
});
});
});
describe("Toggle Indexing Of User Messages Button", () => {
Expand All @@ -103,72 +103,73 @@ describe("Manage Account Menu", () => {
);
const message = await reply(
reacord,
<ManageAccountMenu initalSettings={initialSettings} initalIsGloballyIgnored={false} />
<ManageAccountMenu initialSettings={initialSettings} initialIsGloballyIgnored={false} />
);
const enableIndexingButton = message!.findButtonByLabel(ENABLE_INDEXING_LABEL, reacord);
expect(enableIndexingButton).toBeDefined();
await enableIndexingButton!.click(textChannel, members.guildMemberOwner);

expect(message!.hasButton(ENABLE_INDEXING_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(DISABLE_INDEXING_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: ENABLE_INDEXING_LABEL,
postClickLabel: DISABLE_INDEXING_LABEL,
message: message!,
reacord,
channel: textChannel,
});
const consentButton = message!.findButtonByLabel(GRANT_CONSENT_LABEL, reacord);
expect(consentButton?.disabled).toBeFalsy();
});
it("should disable indexing of user messages", async () => {
const message = await reply(
reacord,
<ManageAccountMenu initalSettings={defaultSettings} initalIsGloballyIgnored={false} />
<ManageAccountMenu initialSettings={defaultSettings} initialIsGloballyIgnored={false} />
);
const disableIndexingButton = message!.findButtonByLabel(DISABLE_INDEXING_LABEL, reacord);
expect(disableIndexingButton).toBeDefined();
await disableIndexingButton!.click(textChannel, members.guildMemberOwner);

expect(message!.hasButton(DISABLE_INDEXING_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(ENABLE_INDEXING_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: DISABLE_INDEXING_LABEL,
postClickLabel: ENABLE_INDEXING_LABEL,
message: message!,
reacord,
channel: textChannel,
});
const consentButton = message!.findButtonByLabel(GRANT_CONSENT_LABEL, reacord);
expect(consentButton).toBeDefined();
expect(consentButton?.disabled).toBeTruthy();
});
});
describe("Toggle Globally Ignored Button", () => {
it("should enable globally ignored", async () => {
const message = await reply(
reacord,
<ManageAccountMenu initalSettings={defaultSettings} initalIsGloballyIgnored={false} />
<ManageAccountMenu initialSettings={defaultSettings} initialIsGloballyIgnored={false} />
);
const enableIndexingButton = message!.findButtonByLabel(
GLOBALLY_IGNORE_ACCOUNT_LABEL,
reacord
);
expect(enableIndexingButton).toBeDefined();
await enableIndexingButton!.click(textChannel, members.guildMemberOwner);

expect(message!.hasButton(GLOBALLY_IGNORE_ACCOUNT_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(STOP_IGNORING_ACCOUNT_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: GLOBALLY_IGNORE_ACCOUNT_LABEL,
postClickLabel: STOP_IGNORING_ACCOUNT_LABEL,
message: message!,
reacord,
channel: textChannel,
});
});
it("should disable globally ignored", async () => {
await deleteDiscordAccount(toAODiscordAccount(members.guildMemberOwner.user).id);

const message = await reply(
reacord,
<ManageAccountMenu
initalSettings={getDefaultUserServerSettingsWithFlags({
initialSettings={getDefaultUserServerSettingsWithFlags({
serverId: guild.id,
userId: members.guildMemberOwner.id,
})}
initalIsGloballyIgnored={true}
initialIsGloballyIgnored={true}
/>
);
const disableIndexingButton = message!.findButtonByLabel(
STOP_IGNORING_ACCOUNT_LABEL,
reacord
);
expect(disableIndexingButton).toBeDefined();
await disableIndexingButton!.click(textChannel, members.guildMemberOwner);

expect(message!.hasButton(STOP_IGNORING_ACCOUNT_LABEL, reacord)).toBeFalsy();
const button = message!.findButtonByLabel(GLOBALLY_IGNORE_ACCOUNT_LABEL, reacord);
expect(button).toBeDefined();
await toggleButtonTest({
clicker: members.guildMemberOwner,
preClickLabel: STOP_IGNORING_ACCOUNT_LABEL,
postClickLabel: GLOBALLY_IGNORE_ACCOUNT_LABEL,
message: message!,
reacord,
channel: textChannel,
});
});
});
});
Loading