mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-17 01:59:08 +00:00
chore: move src/common/utils to @roleypoly/misc-utils
This commit is contained in:
parent
a374030438
commit
65a8760e86
36 changed files with 38 additions and 465 deletions
|
@ -6,6 +6,7 @@
|
|||
"start": "yarn workspace @roleypoly/worker-emulator start --basePath `pwd`"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roleypoly/misc-utils": "*",
|
||||
"@roleypoly/types": "*",
|
||||
"@roleypoly/worker-emulator": "*",
|
||||
"ksuid": "^2.0.0",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||
import {
|
||||
evaluatePermission,
|
||||
permissions as Permissions,
|
||||
} from '../../../src/common/utils/hasPermission';
|
||||
} from '@roleypoly/misc-utils/hasPermission';
|
||||
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||
import { Handler } from '../router';
|
||||
import { rootUsers, uiPublicURI } from './config';
|
||||
import { Sessions, WrappedKVNamespace } from './kv';
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||
import {
|
||||
Features,
|
||||
Guild,
|
||||
|
@ -6,7 +7,6 @@ import {
|
|||
Role,
|
||||
RoleSafety,
|
||||
} from '@roleypoly/types';
|
||||
import { evaluatePermission, permissions } from '../../../src/common/utils/hasPermission';
|
||||
import { AuthType, cacheLayer, discordFetch } from './api-tools';
|
||||
import { botClientID, botToken } from './config';
|
||||
import { GuildData, Guilds } from './kv';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { withContext } from '@roleypoly/misc-utils/withContext';
|
||||
import * as React from 'react';
|
||||
import { withContext } from '../../../../src/common/utils/withContext';
|
||||
|
||||
export type ScreenSize = {
|
||||
onSmallScreen: boolean;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { FeatureFlagDecorator } from '@roleypoly/misc-utils/featureFlags/react/storyDecorator';
|
||||
import * as React from 'react';
|
||||
import { FeatureFlagDecorator } from '../../../../src/common/utils/featureFlags/react/storyDecorator';
|
||||
import { FeatureGate } from './FeatureGate';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
FeatureFlag,
|
||||
FeatureFlagsContext,
|
||||
} from '../../../../src/common/utils/featureFlags/react';
|
||||
} from '@roleypoly/misc-utils/featureFlags/react';
|
||||
import * as React from 'react';
|
||||
|
||||
export type FeatureGateProps = {
|
||||
featureFlag: FeatureFlag;
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { numberToChroma } from '@roleypoly/design-system/atoms/colors';
|
||||
import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission';
|
||||
import { Role as RPCRole, RoleSafety } from '@roleypoly/types';
|
||||
import chroma from 'chroma-js';
|
||||
import * as React from 'react';
|
||||
import { FaCheck, FaTimes } from 'react-icons/fa';
|
||||
import {
|
||||
evaluatePermission,
|
||||
permissions,
|
||||
} from '../../../../src/common/utils/hasPermission';
|
||||
import * as styled from './Role.styled';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { NavSlug } from '@roleypoly/design-system/molecules/nav-slug';
|
||||
import { sortBy } from '@roleypoly/misc-utils/sortBy';
|
||||
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
|
||||
import * as React from 'react';
|
||||
import Scrollbars from 'react-custom-scrollbars';
|
||||
import { GoStar, GoZap } from 'react-icons/go';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { sortBy } from '../../../../src/common/utils/sortBy';
|
||||
import { GuildNavItem } from './GuildNav.styled';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Role } from '@roleypoly/design-system/atoms/role';
|
||||
import { AmbientLarge, LargeText } from '@roleypoly/design-system/atoms/typography';
|
||||
import { sortBy } from '@roleypoly/misc-utils/sortBy';
|
||||
import { Category as RPCCategory, Role as RPCRole, RoleSafety } from '@roleypoly/types';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import styled from 'styled-components';
|
||||
import { sortBy } from '../../../../src/common/utils/sortBy';
|
||||
import { Head, HeadSub, HeadTitle } from './PickerCategory.styled';
|
||||
|
||||
export type CategoryProps = {
|
||||
|
|
|
@ -4,6 +4,8 @@ import { Link } from '@roleypoly/design-system/atoms/typography';
|
|||
import { PickerCategory } from '@roleypoly/design-system/molecules/picker-category';
|
||||
import { ResetSubmit } from '@roleypoly/design-system/molecules/reset-submit';
|
||||
import { ServerMasthead } from '@roleypoly/design-system/molecules/server-masthead';
|
||||
import { ReactifyNewlines } from '@roleypoly/misc-utils/ReactifyNewlines';
|
||||
import { sortBy } from '@roleypoly/misc-utils/sortBy';
|
||||
import {
|
||||
Category,
|
||||
CategoryType,
|
||||
|
@ -15,8 +17,6 @@ import {
|
|||
import { isEqual, xor } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { GoInfo } from 'react-icons/go';
|
||||
import { ReactifyNewlines } from '../../../../src/common/utils/ReactifyNewlines';
|
||||
import { sortBy } from '../../../../src/common/utils/sortBy';
|
||||
import {
|
||||
CategoryContainer,
|
||||
Container,
|
||||
|
|
|
@ -3,11 +3,11 @@ import { Button } from '@roleypoly/design-system/atoms/button';
|
|||
import { DotOverlay } from '@roleypoly/design-system/atoms/dot-overlay';
|
||||
import { Hero } from '@roleypoly/design-system/atoms/hero';
|
||||
import { AccentTitle, SmallTitle } from '@roleypoly/design-system/atoms/typography';
|
||||
import { evaluatePermission } from '@roleypoly/misc-utils/hasPermission';
|
||||
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
|
||||
import * as React from 'react';
|
||||
import { FaDiscord } from 'react-icons/fa';
|
||||
import { GoArrowLeft } from 'react-icons/go';
|
||||
import { evaluatePermission } from '../../../../src/common/utils/hasPermission';
|
||||
import { FlexLine, FlexWrap } from './ServerSetup.styled';
|
||||
|
||||
export type ServerSetupProps = {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { CompletelyStylelessLink } from '@roleypoly/design-system/atoms/typography';
|
||||
import { ServerListingCard } from '@roleypoly/design-system/molecules/server-listing-card';
|
||||
import { sortBy } from '@roleypoly/misc-utils/sortBy';
|
||||
import { GuildSlug } from '@roleypoly/types';
|
||||
import * as React from 'react';
|
||||
import { sortBy } from '../../../../src/common/utils/sortBy';
|
||||
import { CardContainer, ContentContainer } from './ServersListing.styled';
|
||||
|
||||
type ServersListingProps = {
|
||||
|
|
9
packages/misc-utils/ReactifyNewlines.spec.tsx
Normal file
9
packages/misc-utils/ReactifyNewlines.spec.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { ReactifyNewlines } from './ReactifyNewlines';
|
||||
|
||||
it('renders a correct number of divs per newlines', () => {
|
||||
const view = shallow(<ReactifyNewlines>{`1\n2\n3`}</ReactifyNewlines>);
|
||||
|
||||
expect(view.find('div').length).toBe(3);
|
||||
});
|
12
packages/misc-utils/ReactifyNewlines.tsx
Normal file
12
packages/misc-utils/ReactifyNewlines.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const ReactifyNewlines = (props: { children: string }) => {
|
||||
const textArray = props.children.split('\n');
|
||||
return (
|
||||
<>
|
||||
{textArray.map((part, idx) => (
|
||||
<div key={`rifynl${idx}`}>{part || <> </>}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
19
packages/misc-utils/featureFlags/react/FeatureFlags.tsx
Normal file
19
packages/misc-utils/featureFlags/react/FeatureFlags.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export enum FeatureFlag {
|
||||
AllowListsBlockLists = 'AllowListsBlockLists',
|
||||
}
|
||||
|
||||
export class FeatureFlagProvider {
|
||||
activeFlags: FeatureFlag[] = [];
|
||||
|
||||
constructor(flags: FeatureFlag[] = []) {
|
||||
this.activeFlags = flags;
|
||||
}
|
||||
|
||||
has(flag: FeatureFlag) {
|
||||
return this.activeFlags.includes(flag);
|
||||
}
|
||||
}
|
||||
|
||||
export const FeatureFlagsContext = React.createContext(new FeatureFlagProvider());
|
1
packages/misc-utils/featureFlags/react/index.ts
Normal file
1
packages/misc-utils/featureFlags/react/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './FeatureFlags';
|
12
packages/misc-utils/featureFlags/react/storyDecorator.tsx
Normal file
12
packages/misc-utils/featureFlags/react/storyDecorator.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import { FeatureFlag, FeatureFlagProvider, FeatureFlagsContext } from './FeatureFlags';
|
||||
|
||||
export const FeatureFlagDecorator = (flags: FeatureFlag[]) => (
|
||||
storyFn: () => React.ReactNode
|
||||
) => {
|
||||
return (
|
||||
<FeatureFlagsContext.Provider value={new FeatureFlagProvider(flags)}>
|
||||
{storyFn()}
|
||||
</FeatureFlagsContext.Provider>
|
||||
);
|
||||
};
|
47
packages/misc-utils/hasPermission.spec.ts
Normal file
47
packages/misc-utils/hasPermission.spec.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { roleCategory } from '@roleypoly/design-system/fixtures/storyData';
|
||||
import { Role } from '@roleypoly/types';
|
||||
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[] = [
|
||||
{
|
||||
...roleCategory[0],
|
||||
permissions: String(permissions.ADMINISTRATOR),
|
||||
},
|
||||
{
|
||||
...roleCategory[0],
|
||||
permissions: String(
|
||||
permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME
|
||||
),
|
||||
},
|
||||
{
|
||||
...roleCategory[0],
|
||||
permissions: String(permissions.BAN_MEMBERS),
|
||||
},
|
||||
];
|
||||
|
||||
it('finds a permission within a list of roles', () => {
|
||||
const result = hasPermission(roles, permissions.CHANGE_NICKNAME);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('finds admin within a list of roles', () => {
|
||||
const result = hasPermissionOrAdmin(roles, permissions.BAN_MEMBERS);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('does not find a permission within a list of roles without one', () => {
|
||||
const result = hasPermission(roles, permissions.KICK_MEMBERS);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
55
packages/misc-utils/hasPermission.ts
Normal file
55
packages/misc-utils/hasPermission.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Role } from '@roleypoly/types';
|
||||
|
||||
export const evaluatePermission = <T extends number | bigint>(
|
||||
haystack: T,
|
||||
needle: T
|
||||
): boolean => {
|
||||
return (haystack & needle) === needle;
|
||||
};
|
||||
|
||||
export const hasPermission = (roles: Role[], permission: bigint): boolean => {
|
||||
const aggregateRoles = roles.reduce(
|
||||
(acc, role) => acc | BigInt(role.permissions),
|
||||
BigInt(0)
|
||||
);
|
||||
return evaluatePermission(aggregateRoles, permission);
|
||||
};
|
||||
|
||||
export const hasPermissionOrAdmin = (roles: Role[], permission: bigint): boolean =>
|
||||
hasPermission(roles, permission | permissions.ADMINISTRATOR);
|
||||
|
||||
export const permissions = {
|
||||
// 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_ROLES: BigInt(0x10000000),
|
||||
// MANAGE_WEBHOOKS: BigInt(0x20000000),
|
||||
// MANAGE_EMOJIS: BigInt(0x40000000),
|
||||
};
|
1
packages/misc-utils/isBrowser.ts
Normal file
1
packages/misc-utils/isBrowser.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const isBrowser = () => typeof window !== 'undefined';
|
11
packages/misc-utils/package.json
Normal file
11
packages/misc-utils/package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@roleypoly/misc-utils",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@roleypoly/types": "*"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"enzyme": "3.x",
|
||||
"react": "*"
|
||||
}
|
||||
}
|
48
packages/misc-utils/sortBy.spec.ts
Normal file
48
packages/misc-utils/sortBy.spec.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { sortBy } from './sortBy';
|
||||
|
||||
it('sorts an array of objects by its key', () => {
|
||||
const output = sortBy(
|
||||
[
|
||||
{
|
||||
name: 'bbb',
|
||||
},
|
||||
{
|
||||
name: 'aaa',
|
||||
},
|
||||
{
|
||||
name: 'ddd',
|
||||
},
|
||||
{
|
||||
name: 'ccc',
|
||||
},
|
||||
],
|
||||
'name'
|
||||
);
|
||||
|
||||
expect(output.map((v) => v.name)).toEqual(['aaa', 'bbb', 'ccc', 'ddd']);
|
||||
});
|
||||
|
||||
it('sorts an array of objects by its key with a predicate', () => {
|
||||
const output = sortBy(
|
||||
[
|
||||
{
|
||||
name: 'cc',
|
||||
},
|
||||
{
|
||||
name: 'bbb',
|
||||
},
|
||||
{
|
||||
name: 'aaaa',
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
},
|
||||
],
|
||||
'name',
|
||||
(a, b) => {
|
||||
return a.length > b.length ? 1 : -1;
|
||||
}
|
||||
);
|
||||
|
||||
expect(output.map((v) => v.name)).toEqual(['d', 'cc', 'bbb', 'aaaa']);
|
||||
});
|
21
packages/misc-utils/sortBy.ts
Normal file
21
packages/misc-utils/sortBy.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
export const sortBy = <T, Key extends keyof T>(
|
||||
array: T[],
|
||||
key: Key,
|
||||
predicate?: (a: T[typeof key], b: T[typeof key]) => number
|
||||
) => {
|
||||
return array.sort((a, b) => {
|
||||
if (predicate) {
|
||||
return predicate(a[key], b[key]);
|
||||
}
|
||||
|
||||
if (a[key] === b[key]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (a[key] > b[key]) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
};
|
11
packages/misc-utils/withContext/contextTestHelpers.tsx
Normal file
11
packages/misc-utils/withContext/contextTestHelpers.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export type ContextShimProps<T> = {
|
||||
context: React.Context<T>;
|
||||
children: (data: T) => any;
|
||||
};
|
||||
|
||||
export function ContextShim<T>(props: ContextShimProps<T>) {
|
||||
const context = React.useContext(props.context);
|
||||
return <>{props.children(context)}</>;
|
||||
}
|
2
packages/misc-utils/withContext/index.ts
Normal file
2
packages/misc-utils/withContext/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * as testHelpers from './contextTestHelpers';
|
||||
export * from './withContext';
|
10
packages/misc-utils/withContext/withContext.tsx
Normal file
10
packages/misc-utils/withContext/withContext.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const withContext = <T, K extends T>(
|
||||
Context: React.Context<T>,
|
||||
Component: React.ComponentType<K>
|
||||
): React.FunctionComponent<K> => (props) => (
|
||||
<Context.Consumer>
|
||||
{(context) => <Component {...props} {...context} />}
|
||||
</Context.Consumer>
|
||||
);
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@reach/router": "^1.3.4",
|
||||
"@roleypoly/design-system": "*",
|
||||
"@roleypoly/misc-utils": "*",
|
||||
"@roleypoly/types": "*",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.5",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue