diff --git a/web/roles/community-roles-modal.react.js b/web/roles/community-roles-modal.react.js --- a/web/roles/community-roles-modal.react.js +++ b/web/roles/community-roles-modal.react.js @@ -8,6 +8,7 @@ import type { ThreadInfo } from 'lib/types/thread-types.js'; import css from './community-roles-modal.css'; +import CreateRolesModal from './create-roles-modal.react.js'; import RolePanelEntry from './role-panel-entry.react.js'; import Button, { buttonThemes } from '../components/button.react.js'; import Modal from '../modals/modal.react.js'; @@ -18,7 +19,7 @@ }; function CommunityRolesModal(props: CommunityRolesModalProps): React.Node { - const { popModal } = useModalContext(); + const { popModal, pushModal } = useModalContext(); const { threadInfo } = props; const [updatedThreadInfo, setUpdatedThreadInfo] = @@ -56,7 +57,20 @@ return rolePanelEntries; }, [roleNamesToMembers]); - const onClickCreateRole = React.useCallback(() => {}, []); + const rolePermissionsForNewRole = React.useMemo(() => [], []); + + const onClickCreateRole = React.useCallback( + () => + pushModal( + , + ), + [pushModal, updatedThreadInfo, rolePermissionsForNewRole], + ); return ( diff --git a/web/roles/create-roles-modal.css b/web/roles/create-roles-modal.css new file mode 100644 --- /dev/null +++ b/web/roles/create-roles-modal.css @@ -0,0 +1,72 @@ +.formContainer { + display: flex; + flex-direction: column; + padding: 16px 32px 0 32px; +} + +.roleNameLabel { + color: var(--create-roles-text-color); + padding: 4px 0; + margin: 20px 0 0 0; +} + +.roleNameInput { + display: flex; + color: var(--fg); + margin: 8px 0 12px 0; +} + +.separator { + border: 0; + margin: 16px 32px 8px 32px; + width: 90%; + align-self: center; + height: 2px; + border: none; + border-top: var(--modal-separator) solid 1px; +} + +.permissionsHeaderContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 16px 32px 24px 32px; +} + +.permissionsLabel { + color: var(--create-roles-text-color); +} + +.clearPermissions { + font-size: var(--s-font-14); +} + +.clearPermissionsDisabled { + color: var(--color-disabled); + cursor: not-allowed; +} + +.clearPermissionsEnabled { + color: var(--purple-link); + cursor: pointer; +} + +.permissionsContainer { + max-height: 350px; + overflow-y: auto; +} + +.buttonsContainer { + display: flex; + flex-direction: row; + justify-content: flex-end; + padding: 16px 32px; +} + +.backButton { + width: 100px; +} + +.createRoleButton { + margin-left: 8px; +} diff --git a/web/roles/create-roles-modal.react.js b/web/roles/create-roles-modal.react.js new file mode 100644 --- /dev/null +++ b/web/roles/create-roles-modal.react.js @@ -0,0 +1,148 @@ +// @flow + +import classNames from 'classnames'; +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import { type UserSurfacedPermission } from 'lib/types/thread-permission-types.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; + +import css from './create-roles-modal.css'; +import { + modifyUserSurfacedPermissionOptions, + type ModifiedUserSurfacedPermissionOption, +} from './role-utils.react.js'; +import Button, { buttonThemes } from '../components/button.react.js'; +import EnumSettingsOption from '../components/enum-settings-option.react.js'; +import Input from '../modals/input.react.js'; +import Modal from '../modals/modal.react.js'; + +type CreateRolesModalProps = { + +threadInfo: ThreadInfo, + +action: 'create_role' | 'edit_role', + +roleName: string, + +rolePermissions: $ReadOnlyArray, +}; + +function CreateRolesModal(props: CreateRolesModalProps): React.Node { + const { popModal } = useModalContext(); + const { threadInfo, roleName, rolePermissions } = props; + + const [pendingRoleName, setPendingRoleName] = + React.useState(roleName); + const [pendingRolePermissions, setPendingRolePermissions] = + React.useState<$ReadOnlyArray>(rolePermissions); + + const onChangeRoleName = React.useCallback( + (event: SyntheticEvent) => { + setPendingRoleName(event.currentTarget.value); + }, + [], + ); + + const onCloseModal = React.useCallback(() => { + popModal(); + }, [popModal]); + + const clearPermissionsClassNames = classNames({ + [css.clearPermissions]: true, + [css.clearPermissionsDisabled]: pendingRolePermissions.length === 0, + [css.clearPermissionsEnabled]: pendingRolePermissions.length > 0, + }); + + const onClearPermissions = React.useCallback( + () => setPendingRolePermissions([]), + [], + ); + + const isUserSurfacedPermissionSelected = React.useCallback( + (option: ModifiedUserSurfacedPermissionOption) => + pendingRolePermissions.includes(option.userSurfacedPermission), + [pendingRolePermissions], + ); + + const onEnumValuePress = React.useCallback( + (option: ModifiedUserSurfacedPermissionOption) => + setPendingRolePermissions(currentPermissions => { + if (currentPermissions.includes(option.userSurfacedPermission)) { + return currentPermissions.filter( + permission => permission !== option.userSurfacedPermission, + ); + } else { + return [...currentPermissions, option.userSurfacedPermission]; + } + }), + [], + ); + + const modifiedUserSurfacedPermissionOptions = React.useMemo( + () => modifyUserSurfacedPermissionOptions(threadInfo), + [threadInfo], + ); + + const permissionsList = React.useMemo( + () => + [...modifiedUserSurfacedPermissionOptions].map(permission => ( + onEnumValuePress(permission)} + icon={null} + title={permission.title} + type="checkbox" + statements={permission.statements} + /> + )), + [ + modifiedUserSurfacedPermissionOptions, + isUserSurfacedPermissionSelected, + onEnumValuePress, + ], + ); + + return ( + +
+
Role Name
+
+ +
+
+
+
+
Permissions
+
+ Clear Permissions +
+
+
{permissionsList}
+
+ + +
+
+ ); +} + +export default CreateRolesModal; diff --git a/web/roles/role-utils.react.js b/web/roles/role-utils.react.js new file mode 100644 --- /dev/null +++ b/web/roles/role-utils.react.js @@ -0,0 +1,54 @@ +// @flow + +import { + type UserSurfacedPermissionOption, + userSurfacedPermissionOptions, + userSurfacedPermissions, +} from 'lib/types/thread-permission-types.js'; +import { threadTypes } from 'lib/types/thread-types-enum.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; + +export type ModifiedUserSurfacedPermissionOption = { + ...UserSurfacedPermissionOption, + statements: $ReadOnlyArray<{ + +statement: string, + +isStatementValid: boolean, + +styleStatementBasedOnValidity: boolean, + }>, +}; +function modifyUserSurfacedPermissionOptions( + threadInfo: ThreadInfo, +): $ReadOnlyArray { + // If the thread is a community announcement root, we want to allow + // the option to be voiced in the announcement channels. Otherwise, + // we want to remove that option from being configured since this will + // be guaranteed on the keyserver. + const filteredOptions = + threadInfo.type === threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT + ? userSurfacedPermissionOptions + : [...userSurfacedPermissionOptions].filter( + option => + option.userSurfacedPermission !== + userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, + ); + + // EnumSettingsOption on web requires a statements array, and since this + // is web-specific rather than cross-platform, we should handle this + // here and modify the options to include a statement. + const modifiedOptions = [...filteredOptions].map(option => { + return { + ...option, + statements: [ + { + statement: option.description, + isStatementValid: true, + styleStatementBasedOnValidity: false, + }, + ], + }; + }); + + return modifiedOptions; +} + +export { modifyUserSurfacedPermissionOptions }; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -241,4 +241,5 @@ --modal-secondary-label: var(--shades-black-60); --community-roles-text-color: var(--shades-white-100); --modal-separator: var(--shades-black-80); + --create-roles-text-color: var(--shades-white-100); }