chore: move src/common/utils to @roleypoly/misc-utils

This commit is contained in:
41666 2021-03-12 16:54:08 -05:00
parent a374030438
commit 65a8760e86
36 changed files with 38 additions and 465 deletions

View file

@ -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",

View file

@ -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';

View file

@ -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';

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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,

View file

@ -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 = {

View file

@ -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 = {

View 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);
});

View 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 || <>&nbsp;</>}</div>
))}
</>
);
};

View 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());

View file

@ -0,0 +1 @@
export * from './FeatureFlags';

View 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>
);
};

View 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);
});

View 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),
};

View file

@ -0,0 +1 @@
export const isBrowser = () => typeof window !== 'undefined';

View file

@ -0,0 +1,11 @@
{
"name": "@roleypoly/misc-utils",
"version": "0.1.0",
"dependencies": {
"@roleypoly/types": "*"
},
"optionalDependencies": {
"enzyme": "3.x",
"react": "*"
}
}

View 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']);
});

View 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;
});
};

View 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)}</>;
}

View file

@ -0,0 +1,2 @@
export * as testHelpers from './contextTestHelpers';
export * from './withContext';

View 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>
);

View file

@ -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",