feat(api): add get-picker-data; refactor fully away from old gRPC datatypes

This commit is contained in:
41666 2020-12-15 21:32:46 -05:00
parent 823760dc2f
commit 0b384bfe5c
22 changed files with 359 additions and 151 deletions

View file

@ -108,4 +108,4 @@
"typescript": "^4.1.3",
"webpack": "4.33.0"
}
}
}

View file

@ -63,7 +63,7 @@ const server = http.createServer((req, res) => {
}
res.statusCode = response.status;
loggedStatus = String(response.status);
Object.entries(response.headers).forEach(([k, v]) => res.setHeader(k, v));
response.headers.forEach((value, key) => res.setHeader(key, value));
res.end(response.body);
} catch (e) {
console.error(e);

View file

@ -0,0 +1,78 @@
import {
DiscordUser,
GuildSlug,
PresentableGuild,
SessionData,
} from 'roleypoly/common/types';
import { respond, withSession } from '../utils/api-tools';
import { getGuild, getGuildData, getGuildMemberRoles } from '../utils/guild';
const fail = respond(
{
error: 'guild not found',
},
{
status: 404,
}
);
export const GetPickerData = withSession(
(session?: SessionData) => async (request: Request): Promise<Response> => {
const url = new URL(request.url);
const [, , guildID] = url.pathname.split('/');
if (!guildID) {
return respond(
{
error: 'missing guild id',
},
{
status: 400,
}
);
}
const { id: userID } = session?.user as DiscordUser;
const guilds = session?.guilds as GuildSlug[];
// Save a Discord API request by checking if this user is a member by session first
const checkGuild = guilds.find((guild) => guild.id === guildID);
if (!checkGuild) {
return fail;
}
const guild = await getGuild(guildID, {
skipCachePull: url.searchParams.has('__no_cache'),
});
if (!guild) {
return fail;
}
const memberRolesP = getGuildMemberRoles({
serverID: guildID,
userID,
});
const guildDataP = getGuildData(guildID);
const [guildData, memberRoles] = await Promise.all([guildDataP, memberRolesP]);
if (!memberRoles) {
return fail;
}
const presentableGuild: PresentableGuild = {
id: guildID,
guild: checkGuild,
roles: guild.roles,
member: {
roles: memberRoles,
},
data: guildData,
};
return respond(presentableGuild);
},
{
mustAuthenticate: true,
}
);

View file

@ -120,11 +120,15 @@ const getGuilds = async (accessToken: string) => {
'Bearer'
);
if (!guilds) {
return [];
}
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
id: guild.id,
name: guild.name,
icon: guild.icon,
permissionLevel: parsePermissions(guild.permissions, guild.owner),
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
}));
return guildSlugs;

View file

@ -1,4 +1,5 @@
import { BotJoin } from './handlers/bot-join';
import { GetPickerData } from './handlers/get-picker-data';
import { GetSession } from './handlers/get-session';
import { GetSlug } from './handlers/get-slug';
import { LoginBounce } from './handlers/login-bounce';
@ -16,6 +17,7 @@ router.add('GET', 'login-bounce', LoginBounce);
router.add('GET', 'login-callback', LoginCallback);
router.add('GET', 'get-session', GetSession);
router.add('GET', 'get-slug', GetSlug);
router.add('GET', 'get-picker-data', GetPickerData);
router.add('GET', 'x-headers', (request) => {
const headers: { [x: string]: string } = {};

View file

@ -28,7 +28,7 @@ export const resolveFailures = (
};
export const parsePermissions = (
permissions: number,
permissions: bigint,
owner: boolean = false
): UserGuildPermissions => {
if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) {
@ -56,6 +56,9 @@ export const getSessionID = (request: Request): { type: string; id: string } | n
return { type, id };
};
const userAgent =
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)';
export const discordFetch = async <T>(
url: string,
auth: string,
@ -64,8 +67,7 @@ export const discordFetch = async <T>(
const response = await fetch('https://discord.com/api/v8' + url, {
headers: {
authorization: `${authType} ${auth}`,
'user-agent':
'DiscordBot (https://github.com/roleypoly/roleypoly, git-main) (+https://roleypoly.com)',
'user-agent': userAgent,
},
});
@ -81,12 +83,17 @@ export const cacheLayer = <Identity, Data>(
keyFactory: (identity: Identity) => string,
missHandler: (identity: Identity) => Promise<Data | null>,
ttlSeconds?: number
) => async (identity: Identity): Promise<Data | null> => {
) => async (
identity: Identity,
options: { skipCachePull?: boolean } = {}
): Promise<Data | null> => {
const key = keyFactory(identity);
const value = await kv.get<Data>(key);
if (value) {
return value;
if (!options.skipCachePull) {
const value = await kv.get<Data>(key);
if (value) {
return value;
}
}
const fallbackValue = await missHandler(identity);

View file

@ -1,7 +1,15 @@
import { Guild, Member, Role, RoleSafety } from 'roleypoly/common/types';
import {
Features,
Guild,
GuildData as GuildDataT,
OwnRoleInfo,
Role,
RoleSafety,
} from 'roleypoly/common/types';
import { evaluatePermission, permissions } from 'roleypoly/common/utils/hasPermission';
import { cacheLayer, discordFetch } from './api-tools';
import { botToken } from './config';
import { Guilds } from './kv';
import { botClientID, botToken } from './config';
import { GuildData, Guilds } from './kv';
type APIGuild = {
// Only relevant stuff
@ -30,24 +38,112 @@ export const getGuild = cacheLayer(
return null;
}
const botMemberRoles =
(await getGuildMemberRoles({
serverID: id,
userID: botClientID,
})) || [];
const highestRolePosition = botMemberRoles.reduce<number>((highest, roleID) => {
const role = guildRaw.roles.find((guildRole) => guildRole.id === roleID);
if (!role) {
return highest;
}
// If highest is a bigger number, it stays the highest.
if (highest > role.position) {
return highest;
}
return role.position;
}, guildRaw.roles.length - 1);
const roles = guildRaw.roles.map<Role>((role) => ({
id: role.id,
name: role.name,
color: role.color,
managed: role.managed,
position: role.position,
permissions: role.permissions,
safety: calculateRoleSafety(role, highestRolePosition),
}));
// Filters the raw guild data into data we actually want
const guild: Guild = {
const guild: Guild & OwnRoleInfo = {
id: guildRaw.id,
name: guildRaw.name,
icon: guildRaw.icon,
roles: guildRaw.roles.map<Role>((role) => ({
...role,
safety: RoleSafety.SAFE, // TODO: calculate safety
})),
roles,
highestRolePosition,
};
return guild;
}
},
60 * 60 * 2 // 2 hour TTL
);
export const getGuildMember = async (
serverID: string,
userID: string
): Promise<Member> => {
return {} as any;
type GuildMemberIdentity = {
serverID: string;
userID: string;
};
type APIMember = {
// Only relevant stuff, again.
roles: string[];
};
export const getGuildMemberRoles = cacheLayer<GuildMemberIdentity, Role['id'][]>(
Guilds,
({ serverID, userID }) => `guilds/${serverID}/members/${userID}`,
async ({ serverID, userID }) => {
const discordMember = await discordFetch<APIMember>(
`/guilds/${serverID}/members/${userID}`,
botToken,
'Bot'
);
if (!discordMember) {
return null;
}
return discordMember.roles;
},
60 * 5 // 5 minute TTL
);
export const getGuildData = async (id: string): Promise<GuildDataT> => {
const guildData = await GuildData.get<GuildDataT>(id);
if (!guildData) {
return {
id,
message: '',
categories: [],
features: Features.None,
};
}
return guildData;
};
const calculateRoleSafety = (role: Role | APIRole, highestBotRolePosition: number) => {
let safety = RoleSafety.Safe;
if (role.managed) {
safety |= RoleSafety.ManagedRole;
}
if (role.position > highestBotRolePosition) {
safety |= RoleSafety.HigherThanBot;
}
const permBigInt = BigInt(role.permissions);
if (
evaluatePermission(permBigInt, permissions.ADMINISTRATOR) ||
evaluatePermission(permBigInt, permissions.MANAGE_ROLES)
) {
safety |= RoleSafety.DangerousPermissions;
}
return safety;
};

View file

@ -1,12 +1,12 @@
export enum CategoryType {
SINGLE = 0,
MULTI,
Single = 0,
Multi,
}
export type Category = {
id: string;
name: string;
rolesList: string[];
roles: string[];
hidden: boolean;
type: CategoryType;
position: number;

View file

@ -9,34 +9,34 @@ export type Guild = {
roles: Role[];
};
export type GuildRoles = {
id: string;
rolesList: Role[];
};
export enum Features {
None,
Preview = None,
}
export type GuildData = {
id: string;
message: string;
categoriesList: Category[];
entitlementsList: string[];
categories: Category[];
features: Features;
};
export type PresentableGuild = {
id: string;
guild: Guild;
guild: GuildSlug;
member: Member;
data: GuildData;
roles: GuildRoles;
roles: Role[];
};
export type GuildEnumeration = {
guildsList: PresentableGuild[];
guilds: PresentableGuild[];
};
export enum UserGuildPermissions {
User,
Manager,
Admin,
Manager = 1 << 1,
Admin = 1 << 2,
}
export type GuildSlug = {

View file

@ -1,7 +1,8 @@
export enum RoleSafety {
SAFE = 0,
HIGHERTHANBOT,
DANGEROUSPERMISSIONS,
Safe = 0,
HigherThanBot = 1 << 1,
DangerousPermissions = 1 << 2,
ManagedRole = 1 << 3,
}
export type Role = {
@ -14,3 +15,7 @@ export type Role = {
/** Permissions is should be used as a BigInt, NOT a number. */
permissions: string;
};
export type OwnRoleInfo = {
highestRolePosition: number;
};

View file

@ -7,10 +7,10 @@ export type DiscordUser = {
};
export type Member = {
guildid: string;
rolesList: string[];
nick: string;
user: DiscordUser;
guildid?: string;
roles: string[];
nick?: string;
user?: DiscordUser;
};
export type RoleypolyUser = {

View file

@ -2,10 +2,10 @@ import {
Category,
CategoryType,
DiscordUser,
Features,
Guild,
GuildData,
GuildEnumeration,
GuildRoles,
GuildSlug,
Member,
Role,
@ -21,7 +21,7 @@ export const roleCategory: Role[] = [
color: 0xffc0cb,
position: 1,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'bbb',
@ -30,7 +30,7 @@ export const roleCategory: Role[] = [
color: 0xc0ebff,
position: 2,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'ccc',
@ -39,7 +39,7 @@ export const roleCategory: Role[] = [
color: 0xc0ffd5,
position: 3,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'ddd',
@ -48,7 +48,7 @@ export const roleCategory: Role[] = [
color: 0xff0000,
position: 4,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'eee',
@ -57,7 +57,7 @@ export const roleCategory: Role[] = [
color: 0x000000,
position: 5,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'fff',
@ -66,7 +66,7 @@ export const roleCategory: Role[] = [
color: 0x1,
position: 6,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'unsafe1',
@ -75,7 +75,7 @@ export const roleCategory: Role[] = [
color: 0xff0088,
position: 7,
managed: false,
safety: RoleSafety.HIGHERTHANBOT,
safety: RoleSafety.HigherThanBot,
},
{
id: 'unsafe2',
@ -84,16 +84,16 @@ export const roleCategory: Role[] = [
color: 0x00ff88,
position: 8,
managed: false,
safety: RoleSafety.DANGEROUSPERMISSIONS,
safety: RoleSafety.DangerousPermissions,
},
];
export const mockCategory: Category = {
id: 'aaa',
name: 'Mock',
rolesList: roleCategory.map((x) => x.id),
roles: roleCategory.map((x) => x.id),
hidden: false,
type: CategoryType.MULTI,
type: CategoryType.Multi,
position: 0,
};
@ -105,7 +105,7 @@ export const roleCategory2: Role[] = [
color: 0xff0000,
position: 9,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
{
id: 'eee2',
@ -114,24 +114,19 @@ export const roleCategory2: Role[] = [
color: 0x00ff00,
position: 10,
managed: false,
safety: RoleSafety.SAFE,
safety: RoleSafety.Safe,
},
];
export const mockCategorySingle: Category = {
id: 'bbb',
name: 'Mock Single 岡野',
rolesList: roleCategory2.map((x) => x.id),
roles: roleCategory2.map((x) => x.id),
hidden: false,
type: CategoryType.SINGLE,
type: CategoryType.Single,
position: 0,
};
export const guildRoles: GuildRoles = {
id: 'aaa',
rolesList: [...roleCategory, ...roleCategory2],
};
export const roleWikiData = {
aaa: 'Typically used by feminine-identifying people',
bbb: 'Typically used by masculine-identifying people',
@ -145,33 +140,38 @@ export const guild: Guild = {
roles: [],
};
export const guildMap: { [x: string]: Guild } = {
'emoji megaporium': guild,
export const guildMap: { [x: string]: GuildSlug } = {
'emoji megaporium': {
name: guild.name,
id: guild.id,
permissionLevel: 0,
icon: guild.icon,
},
Roleypoly: {
name: 'Roleypoly',
id: '203493697696956418',
permissionLevel: 0,
icon: 'ff08d36f5aee1ff48f8377b65d031ab0',
roles: [],
},
'chamber of secrets': {
name: 'chamber of secrets',
id: 'aaa',
permissionLevel: 0,
icon: '',
roles: [],
},
Eclipse: {
name: 'Eclipse',
id: '408821059161423873',
permissionLevel: 0,
icon: '49dfdd8b2456e2977e80a8b577b19c0d',
roles: [],
},
};
export const guildData: GuildData = {
id: 'aaa',
message: 'henlo worl!!',
categoriesList: [mockCategory, mockCategorySingle],
entitlementsList: [],
categories: [mockCategory, mockCategorySingle],
features: Features.None,
};
export const user: DiscordUser = {
@ -184,7 +184,7 @@ export const user: DiscordUser = {
export const member: Member = {
guildid: 'aaa',
rolesList: ['aaa', 'eee', 'unsafe2', 'ddd2'],
roles: ['aaa', 'eee', 'unsafe2', 'ddd2'],
nick: 'okano cat',
user: user,
};
@ -194,42 +194,42 @@ export const rpUser: RoleypolyUser = {
};
export const guildEnum: GuildEnumeration = {
guildsList: [
guilds: [
{
id: 'aaa',
guild: guildMap['emoji megaporium'],
member,
data: guildData,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'bbb',
guild: guildMap['Roleypoly'],
member: {
...member,
rolesList: ['unsafe2'],
roles: ['unsafe2'],
},
data: guildData,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'ccc',
guild: guildMap['chamber of secrets'],
member,
data: guildData,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
},
{
id: 'ddd',
guild: guildMap['Eclipse'],
member,
data: guildData,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
},
],
};
export const mastheadSlugs: GuildSlug[] = guildEnum.guildsList.map<GuildSlug>(
export const mastheadSlugs: GuildSlug[] = guildEnum.guilds.map<GuildSlug>(
(guild, idx) => ({
id: guild.guild.id,
name: guild.guild.name,

View file

@ -1,20 +1,29 @@
import { Role } from 'roleypoly/common/types';
import { guildRoles } from 'roleypoly/common/types/storyData';
import { hasPermission, hasPermissionOrAdmin, permissions } from './hasPermission';
import { roleCategory } from 'roleypoly/common/types/storyData';
import { hasPermission, hasPermissionOrAdmin } from './hasPermission';
export const permissions = {
KICK_MEMBERS: BigInt(0x2),
BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8),
SPEAK: BigInt(0x200000),
CHANGE_NICKNAME: BigInt(0x4000000),
MANAGE_ROLES: BigInt(0x10000000),
};
const roles: Role[] = [
{
...guildRoles.rolesList[0],
...roleCategory[0],
permissions: String(permissions.ADMINISTRATOR),
},
{
...guildRoles.rolesList[0],
...roleCategory[0],
permissions: String(
permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME
),
},
{
...guildRoles.rolesList[0],
...roleCategory[0],
permissions: String(permissions.BAN_MEMBERS),
},
];

View file

@ -19,35 +19,37 @@ export const hasPermissionOrAdmin = (roles: Role[], permission: bigint): boolean
hasPermission(roles, permission | permissions.ADMINISTRATOR);
export const permissions = {
CREATE_INSTANT_INVITE: BigInt(0x1),
KICK_MEMBERS: BigInt(0x2),
BAN_MEMBERS: BigInt(0x4),
// IMPORTANT: Only uncomment what's actually used. All are left for convenience.
// CREATE_INSTANT_INVITE: BigInt(0x1),
// KICK_MEMBERS: BigInt(0x2),
// BAN_MEMBERS: BigInt(0x4),
ADMINISTRATOR: BigInt(0x8),
MANAGE_CHANNELS: BigInt(0x10),
MANAGE_GUILD: BigInt(0x20),
ADD_REACTIONS: BigInt(0x40),
VIEW_AUDIT_LOG: BigInt(0x80),
VIEW_CHANNEL: BigInt(0x400),
SEND_MESSAGES: BigInt(0x800),
SEND_TTS_MESSAGES: BigInt(0x1000),
MANAGE_MESSAGES: BigInt(0x2000),
EMBED_LINKS: BigInt(0x4000),
ATTACH_FILES: BigInt(0x8000),
READ_MESSAGE_HISTORY: BigInt(0x10000),
MENTION_EVERYONE: BigInt(0x20000),
USE_EXTERNAL_EMOJIS: BigInt(0x40000),
VIEW_GUILD_INSIGHTS: BigInt(0x80000),
CONNECT: BigInt(0x100000),
SPEAK: BigInt(0x200000),
MUTE_MEMBERS: BigInt(0x400000),
DEAFEN_MEMBERS: BigInt(0x800000),
MOVE_MEMBERS: BigInt(0x1000000),
USE_VAD: BigInt(0x2000000),
PRIORITY_SPEAKER: BigInt(0x100),
STREAM: BigInt(0x200),
CHANGE_NICKNAME: BigInt(0x4000000),
MANAGE_NICKNAMES: BigInt(0x8000000),
// MANAGE_CHANNELS: BigInt(0x10),
// MANAGE_GUILD: BigInt(0x20),
// ADD_REACTIONS: BigInt(0x40),
// VIEW_AUDIT_LOG: BigInt(0x80),
// VIEW_CHANNEL: BigInt(0x400),
// SEND_MESSAGES: BigInt(0x800),
// SEND_TTS_MESSAGES: BigInt(0x1000),
// MANAGE_MESSAGES: BigInt(0x2000),
// EMBED_LINKS: BigInt(0x4000),
// ATTACH_FILES: BigInt(0x8000),
// READ_MESSAGE_HISTORY: BigInt(0x10000),
// MENTION_EVERYONE: BigInt(0x20000),
// USE_EXTERNAL_EMOJIS: BigInt(0x40000),
// VIEW_GUILD_INSIGHTS: BigInt(0x80000),
// CONNECT: BigInt(0x100000),
// SPEAK: BigInt(0x200000),
// MUTE_MEMBERS: BigInt(0x400000),
// DEAFEN_MEMBERS: BigInt(0x800000),
// MOVE_MEMBERS: BigInt(0x1000000),
// USE_VAD: BigInt(0x2000000),
// PRIORITY_SPEAKER: BigInt(0x100),
// STREAM: BigInt(0x200),
// CHANGE_NICKNAME: BigInt(0x4000000),
// MANAGE_NICKNAMES: BigInt(0x8000000),
MANAGE_ROLES: BigInt(0x10000000),
MANAGE_WEBHOOKS: BigInt(0x20000000),
MANAGE_EMOJIS: BigInt(0x40000000),
// MANAGE_WEBHOOKS: BigInt(0x20000000),
// MANAGE_EMOJIS: BigInt(0x40000000),
};

View file

@ -65,9 +65,9 @@ export const Role = (props: Props) => {
const disabledReason = (role: RPCRole) => {
switch (role.safety) {
case RoleSafety.HIGHERTHANBOT:
case RoleSafety.HigherThanBot:
return `This role is above Roleypoly's own role.`;
case RoleSafety.DANGEROUSPERMISSIONS:
case RoleSafety.DangerousPermissions:
const rolePermissions = BigInt(role.permissions);
let permissionHits: string[] = [];

View file

@ -19,7 +19,7 @@ type Props = {
};
const typeEnumToSwitch = (typeData: CategoryType) => {
if (typeData === CategoryType.SINGLE) {
if (typeData === CategoryType.Single) {
return 'Single';
} else {
return 'Multiple';
@ -28,9 +28,9 @@ const typeEnumToSwitch = (typeData: CategoryType) => {
const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => {
if (typeData === 'Single') {
return CategoryType.SINGLE;
return CategoryType.Single;
} else {
return CategoryType.MULTI;
return CategoryType.Multi;
}
};
@ -53,14 +53,14 @@ export const EditorCategory = (props: Props) => {
updateSearchTerm('');
props.onChange({
...props.category,
rolesList: [...props.category.rolesList, role.id],
roles: [...props.category.roles, role.id],
});
};
const handleRoleDeselect = (role: RoleType) => () => {
props.onChange({
...props.category,
rolesList: props.category.rolesList.filter((x) => x !== role.id),
roles: props.category.roles.filter((x) => x !== role.id),
});
};
@ -122,7 +122,7 @@ export const EditorCategory = (props: Props) => {
onChange={(x) => updateSearchTerm(x.target.value)}
/>
<RoleContainer>
{props.category.rolesList.map((id) => {
{props.category.roles.map((id) => {
const role = props.guildRoles.find((x) => x.id === id);
if (!role) {
return <></>;

View file

@ -56,7 +56,7 @@ export const PickerCategory = (props: CategoryProps) => (
role={role}
selected={props.selectedRoles.includes(role.id)}
onClick={props.onChange(role)}
disabled={role.safety !== RoleSafety.SAFE}
disabled={role.safety !== RoleSafety.Safe}
tooltipId={props.category.id}
/>
</Container>

View file

@ -17,12 +17,12 @@ export const EditorShell = (props: Props) => (
const RolesTab = (props: Props) => (
<div>
{props.guild.data.categoriesList.map((category, idx) => (
{props.guild.data.categories.map((category, idx) => (
<CategoryContainer key={idx}>
<EditorCategory
category={category}
uncategorizedRoles={[]}
guildRoles={props.guild.roles.rolesList}
guildRoles={props.guild.roles}
onChange={(x) => console.log(x)}
/>
</CategoryContainer>

View file

@ -8,9 +8,10 @@ import * as React from 'react';
import {
guild,
guildData,
guildRoles,
member,
mockCategorySingle,
roleCategory,
roleCategory2,
} from 'roleypoly/common/types/storyData';
import { Role } from 'roleypoly/design-system/atoms/role';
import { PickerCategory } from 'roleypoly/design-system/molecules/picker-category';
@ -19,9 +20,9 @@ import { RolePicker, RolePickerProps } from './RolePicker';
it('unselects the rest of a category in single mode', () => {
const props: RolePickerProps = {
guildData: { ...guildData, categoriesList: [mockCategorySingle] },
member: { ...member, rolesList: [] },
roles: guildRoles,
guildData: { ...guildData, categories: [mockCategorySingle] },
member: { ...member, roles: [] },
roles: [...roleCategory, ...roleCategory2],
guild: guild,
onSubmit: jest.fn(),
editable: false,
@ -34,10 +35,10 @@ it('unselects the rest of a category in single mode', () => {
roles.first().props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.rolesList[0]]);
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[0]]);
roles.last().props().onClick?.(true);
roles.at(1).props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.rolesList[1]]);
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[1]]);
});

View file

@ -1,12 +1,18 @@
import * as React from 'react';
import { guild, guildData, guildRoles, member } from 'roleypoly/common/types/storyData';
import {
guild,
guildData,
member,
roleCategory,
roleCategory2,
} from 'roleypoly/common/types/storyData';
import { RolePicker, RolePickerProps } from './RolePicker';
const props: Partial<RolePickerProps> = {
guildData: guildData,
member: member,
guild: guild,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
editable: false,
};

View file

@ -5,7 +5,6 @@ import {
CategoryType,
Guild,
GuildData,
GuildRoles,
Member,
Role,
} from 'roleypoly/common/types';
@ -27,7 +26,7 @@ export type RolePickerProps = {
guild: Guild;
guildData: GuildData;
member: Member;
roles: GuildRoles;
roles: Role[];
onSubmit: (selectedRoles: string[]) => void;
editable: boolean;
};
@ -43,15 +42,15 @@ const arrayMatches = (a: any[], b: any[]) => {
export const RolePicker = (props: RolePickerProps) => {
const [selectedRoles, updateSelectedRoles] = React.useState<string[]>(
props.member.rolesList
props.member.roles
);
const handleChange = (category: Category) => (role: Role) => (newState: boolean) => {
if (category.type === CategoryType.SINGLE) {
if (category.type === CategoryType.Single) {
updateSelectedRoles(
newState === true
? [
...selectedRoles.filter((x) => !category.rolesList.includes(x)),
...selectedRoles.filter((x) => !category.roles.includes(x)),
role.id,
]
: selectedRoles.filter((x) => x !== role.id)
@ -79,31 +78,29 @@ export const RolePicker = (props: RolePickerProps) => {
</>
)}
{props.guildData.categoriesList.length !== 0 ? (
{props.guildData.categories.length !== 0 ? (
<>
<div>
{props.guildData.categoriesList.map((category, idx) => (
{props.guildData.categories.map((category, idx) => (
<CategoryContainer key={idx}>
<PickerCategory
key={idx}
category={category}
title={category.name}
selectedRoles={selectedRoles.filter((roleId) =>
category.rolesList.includes(roleId)
category.roles.includes(roleId)
)}
roles={
category.rolesList
category.roles
.map((role) =>
props.roles.rolesList.find(
(r) => r.id === role
)
props.roles.find((r) => r.id === role)
)
.filter((r) => r !== undefined) as Role[]
}
onChange={handleChange(category)}
wikiMode={false}
type={
category.type === CategoryType.SINGLE
category.type === CategoryType.Single
? 'single'
: 'multi'
}
@ -112,12 +109,12 @@ export const RolePicker = (props: RolePickerProps) => {
))}
</div>
<FaderOpacity
isVisible={!arrayMatches(selectedRoles, props.member.rolesList)}
isVisible={!arrayMatches(selectedRoles, props.member.roles)}
>
<ResetSubmit
onSubmit={() => props.onSubmit(selectedRoles)}
onReset={() => {
updateSelectedRoles(props.member.rolesList);
updateSelectedRoles(props.member.roles);
}}
/>
</FaderOpacity>

View file

@ -3,9 +3,10 @@ import {
guild,
guildData,
guildEnum,
guildRoles,
mastheadSlugs,
member,
roleCategory,
roleCategory2,
user,
} from 'roleypoly/common/types/storyData';
import { RolePickerTemplate, RolePickerTemplateProps } from './RolePicker';
@ -19,7 +20,7 @@ const props: RolePickerTemplateProps = {
member: member,
guild: guild,
guilds: mastheadSlugs,
roles: guildRoles,
roles: [...roleCategory, ...roleCategory2],
editable: false,
user: user,
guildEnumeration: guildEnum,