diff --git a/web/components/enum-settings-option-info.react.js b/web/components/enum-settings-option-info.react.js index d7397f9fa..c8b0b16ef 100644 --- a/web/components/enum-settings-option-info.react.js +++ b/web/components/enum-settings-option-info.react.js @@ -1,42 +1,42 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import css from './enum-settings-option-info.css'; type Props = { +optionSelected: boolean, - +valid: boolean, - +styleStatementBasedOnValidity: boolean, + +valid?: boolean, + +styleStatementBasedOnValidity?: boolean, +children: React.Node, }; function EnumSettingsOptionInfo(props: Props): React.Node { const { optionSelected, valid, styleStatementBasedOnValidity, children } = props; const optionInfoClasses = classnames({ [css.optionInfo]: true, [css.optionInfoInvalid]: styleStatementBasedOnValidity && !valid, [css.optionInfoInvalidSelected]: styleStatementBasedOnValidity && !valid && optionSelected, }); const icon = React.useMemo(() => { if (!styleStatementBasedOnValidity) { return null; } return ; }, [styleStatementBasedOnValidity, valid]); return (
{icon} {children}
); } export default EnumSettingsOptionInfo; diff --git a/web/components/enum-settings-option.react.js b/web/components/enum-settings-option.react.js index 13521a07d..e4d59e3b7 100644 --- a/web/components/enum-settings-option.react.js +++ b/web/components/enum-settings-option.react.js @@ -1,99 +1,99 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import Checkbox from './checkbox.react.js'; import EnumSettingsOptionInfo from './enum-settings-option-info.react.js'; import css from './enum-settings-option.css'; import Radio from './radio.react.js'; const iconPositionClassnames = { top: css.optionIconTop, center: css.optionIconCenter, bottom: css.optionIconBottom, }; type InputType = 'radio' | 'checkbox'; type IconPosition = $Keys; type Props = { +selected: boolean, +onSelect: () => void, +disabled?: boolean, +icon: React.Node, +title: string, +type?: InputType, +iconPosition?: IconPosition, +statements: $ReadOnlyArray<{ +statement: string, - +isStatementValid: boolean, - +styleStatementBasedOnValidity: boolean, + +isStatementValid?: boolean, + +styleStatementBasedOnValidity?: boolean, }>, }; function EnumSettingsOption(props: Props): React.Node { const { icon, title, statements, selected, onSelect, disabled = false, type = 'radio', iconPosition = 'center', } = props; const descriptionItems = React.useMemo( () => statements.map( ({ statement, isStatementValid, styleStatementBasedOnValidity }) => ( {statement} ), ), [selected, statements], ); const inputIcon = React.useMemo(() => { if (disabled) { return null; } else if (type === 'checkbox') { return ; } else if (type === 'radio') { return ; } return undefined; }, [disabled, type, selected]); const optionContainerClasses = classnames(css.optionContainer, { [css.optionContainerSelected]: selected, }); const optionIconClasses = classnames( css.optionIcon, iconPositionClassnames[iconPosition], ); return (
{icon}
{title}
{descriptionItems}
{inputIcon}
); } export default EnumSettingsOption; diff --git a/web/roles/community-roles-modal.react.js b/web/roles/community-roles-modal.react.js index 7fb5d359f..77e37c60a 100644 --- a/web/roles/community-roles-modal.react.js +++ b/web/roles/community-roles-modal.react.js @@ -1,82 +1,96 @@ // @flow import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { useRoleMemberCountsForCommunity } from 'lib/shared/thread-utils.js'; 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'; import { useSelector } from '../redux/redux-utils.js'; type CommunityRolesModalProps = { +community: ThreadInfo, }; function CommunityRolesModal(props: CommunityRolesModalProps): React.Node { - const { popModal } = useModalContext(); + const { popModal, pushModal } = useModalContext(); const { community } = props; const [threadInfo, setThreadInfo] = React.useState(community); const threadID = threadInfo.id; const reduxThreadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[threadID], ); React.useEffect(() => { if (reduxThreadInfo) { setThreadInfo(reduxThreadInfo); } }, [reduxThreadInfo]); const roleNamesToMembers = useRoleMemberCountsForCommunity(threadInfo); const rolePanelList = React.useMemo( () => Object.keys(roleNamesToMembers).map(roleName => ( )), [roleNamesToMembers], ); - const onClickCreateRole = React.useCallback(() => {}, []); + const rolePermissionsForNewRole = React.useMemo(() => new Set(), []); + + const onClickCreateRole = React.useCallback( + () => + pushModal( + , + ), + [pushModal, threadInfo, rolePermissionsForNewRole], + ); return (
Roles help you group community members together and assign them certain permissions. When people join the community, they are automatically assigned the Members role.
Communities must always have the Admins and Members role.
Roles
Members

{rolePanelList}
); } export default CommunityRolesModal; diff --git a/web/roles/create-roles-modal.css b/web/roles/create-roles-modal.css new file mode 100644 index 000000000..bf34ddc85 --- /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-top: 20px; +} + +.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 index 000000000..2ee32b02d --- /dev/null +++ b/web/roles/create-roles-modal.react.js @@ -0,0 +1,149 @@ +// @flow + +import classNames from 'classnames'; +import * as React from 'react'; + +import { useModalContext } from 'lib/components/modal-provider.react.js'; +import type { + UserSurfacedPermission, + UserSurfacedPermissionOption, +} from 'lib/types/thread-permission-types.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { useFilterPermissionOptionsByThreadType } from 'lib/utils/role-utils.js'; + +import css from './create-roles-modal.css'; +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: $ReadOnlySet, +}; + +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<$ReadOnlySet>(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.size === 0, + [css.clearPermissionsEnabled]: pendingRolePermissions.size > 0, + }); + + const onClearPermissions = React.useCallback( + () => setPendingRolePermissions(new Set()), + [], + ); + + const isUserSurfacedPermissionSelected = React.useCallback( + (option: UserSurfacedPermissionOption) => + pendingRolePermissions.has(option.userSurfacedPermission), + [pendingRolePermissions], + ); + + const onEnumValuePress = React.useCallback( + (option: UserSurfacedPermissionOption) => + setPendingRolePermissions(currentPermissions => { + if (currentPermissions.has(option.userSurfacedPermission)) { + const newPermissions = new Set(currentPermissions); + newPermissions.delete(option.userSurfacedPermission); + return newPermissions; + } else { + return new Set([ + ...currentPermissions, + option.userSurfacedPermission, + ]); + } + }), + [], + ); + + const filteredUserSurfacedPermissionOptions = + useFilterPermissionOptionsByThreadType(threadInfo.type); + + const permissionsList = React.useMemo( + () => + [...filteredUserSurfacedPermissionOptions].map(permission => ( + onEnumValuePress(permission)} + icon={null} + title={permission.title} + type="checkbox" + statements={[{ statement: permission.description }]} + /> + )), + [ + filteredUserSurfacedPermissionOptions, + isUserSurfacedPermissionSelected, + onEnumValuePress, + ], + ); + + return ( + +
+
Role Name
+
+ +
+
+
+
+
Permissions
+
+ Clear Permissions +
+
+
{permissionsList}
+
+ + +
+
+ ); +} + +export default CreateRolesModal; diff --git a/web/theme.css b/web/theme.css index c89a254e3..3a8ec088d 100644 --- a/web/theme.css +++ b/web/theme.css @@ -1,244 +1,245 @@ :root { /* Never use color values defined here directly in CSS. Add color variables to "Color Theme" below The reason we never use color values defined here directly in CSS is 1. It makes changing themes from light / dark / user generated impossible. 2. Gives the programmer context into the color being used. 3. If our color system changes it's much easier to change color values in one place. Add a color value to the theme below, and then use it in your CSS. naming convention: - bg: background. - fg: foreground. - color: text-color */ --shades-white-100: #ffffff; --shades-white-90: #f5f5f5; --shades-white-80: #ebebeb; --shades-white-70: #e0e0e0; --shades-white-60: #cccccc; --shades-black-100: #0a0a0a; --shades-black-90: #1f1f1f; --shades-black-80: #404040; --shades-black-70: #666666; --shades-black-60: #808080; --violet-dark-100: #7e57c2; --violet-dark-80: #6d49ab; --violet-dark-60: #563894; --violet-dark-40: #44297a; --violet-dark-20: #331f5c; --violet-light-100: #ae94db; --violet-light-80: #b9a4df; --violet-light-60: #d3c6ec; --violet-light-40: #e8e0f5; --violet-light-20: #f3f0fa; --success-light-10: #d5f6e3; --success-light-50: #6cdf9c; --success-primary: #00c853; --success-dark-50: #029841; --success-dark-90: #034920; --error-light-10: #feebe6; --error-light-50: #f9947b; --error-primary: #f53100; --error-dark-50: #b62602; --error-dark-90: #4f1203; --logo-bg: #111827; --spoiler-color: #33332c; --loading-foreground: #1b0e38; --bg: var(--shades-black-100); --fg: var(--shades-white-100); --color-disabled: var(--shades-black-60); --text-input-bg: var(--shades-black-80); --text-input-color: var(--shades-white-60); --text-input-placeholder: var(--shades-white-60); --border: var(--shades-black-80); --error: var(--error-primary); --success: var(--success-dark-50); /* Color Theme */ --btn-bg-filled: var(--violet-dark-100); --btn-bg-outline: var(--shades-black-90); --btn-bg-success: var(--success-dark-50); --btn-bg-danger: var(--error-primary); --btn-bg-disabled: var(--shades-black-80); --btn-disabled-color: var(--shades-black-60); --chat-bg: var(--violet-dark-80); --chat-confirmation-icon: var(--violet-dark-100); --keyserver-selection: var(--violet-dark-60); --thread-selection: var(--violet-light-80); --thread-hover-bg: var(--shades-black-80); --thread-active-bg: var(--shades-black-80); --chat-timestamp-color: var(--shades-black-60); --tool-tip-bg: var(--shades-black-80); --tool-tip-color: var(--shades-white-60); --border-color: var(--shades-black-80); --calendar-chevron: var(--shades-black-60); --calendar-day-bg: var(--shades-black-60); --calendar-day-selected-color: var(--violet-dark-80); --community-bg: var(--shades-black-90); --community-settings-selected: var(--violet-dark-60); --unread-bg: var(--error-primary); --settings-btn-bg: var(--violet-dark-100); --community-creation-btn-bg: var(--shades-black-80); --community-creation-ancestry-bg: var(--shades-black-80); --community-creation-ancestry-text: var(--shades-black-60); --community-creation-form-notice: var(--shades-white-60); --community-creation-keyserver-container: var(--shades-black-100); --modal-bg: var(--shades-black-90); --modal-fg: var(--shades-white-60); --join-bg: var(--shades-black-90); --help-color: var(--shades-black-60); --breadcrumb-color: var(--shades-white-60); --breadcrumb-color-unread: var(--shades-white-60); --btn-outline-border: var(--shades-black-60); --thread-color-read: var(--shades-black-60); --thread-preview-secondary: var(--shades-black-70); --relationship-button-green: var(--success-dark-50); --relationship-button-red: var(--error-primary); --relationship-button-text: var(--fg); --disconnected-bar-alert-bg: var(--error-dark-50); --disconnected-bar-alert-color: var(--shades-white-100); --disconnected-bar-connecting-bg: var(--shades-white-70); --disconnected-bar-connecting-color: var(--shades-black-100); --permission-color: var(--shades-white-60); --thread-top-bar-color: var(--shades-white-100); --thread-top-bar-search-button-color: var(--shades-black-60); --thread-ancestor-keyserver-border: var(--shades-black-70); --thread-ancestor-color: var(--shades-white-100); --thread-ancestor-separator-color: var(--shades-white-60); --text-message-default-background: var(--shades-black-80); --message-action-tooltip-bg: var(--shades-black-90); --message-action-tooltip-bg-light: var(--shades-black-80); --menu-bg: var(--shades-black-90); --menu-bg-light: var(--shades-black-80); --menu-separator-color: var(--shades-black-80); --menu-color: var(--shades-black-60); --menu-color-light: var(--shades-white-60); --menu-color-hover: var(--shades-white-100); --menu-color-dangerous: var(--error-primary); --menu-color-dangerous-hover: var(--error-light-50); --app-list-icon-read-only-color: var(--shades-black-60); --app-list-icon-enabled-color: var(--success-primary); --app-list-icon-disabled-color: var(--shades-white-80); --account-settings-label: var(--shades-black-60); --account-button-color: var(--violet-dark-100); --chat-thread-list-color-active: var(--shades-white-60); --chat-thread-list-menu-color: var(--shades-white-60); --chat-thread-list-menu-bg: var(--shades-black-80); --chat-thread-list-menu-active-color: var(--shades-white-60); --chat-thread-list-menu-active-bg: var(--shades-black-90); --search-clear-color: var(--shades-white-100); --search-clear-bg: var(--shades-black-70); --search-input-color: var(--shades-white-100); --search-input-placeholder: var(--shades-black-60); --search-icon-color: var(--shades-black-60); --tabs-header-active-color: var(--shades-white-100); --tabs-header-active-border: var(--violet-light-100); --tabs-header-active-background: var(--violet-dark-100); --tabs-header-background-color: var(--shades-black-60); --tabs-header-background-color-pill: var(--shades-white-60); --tabs-header-background-border: var(--shades-black-80); --tabs-header-background-color-hover: var(--shades-white-80); --tabs-header-background-border-hover: var(--shades-black-70); --members-modal-member-text: var(--shades-black-60); --members-modal-member-text-hover: var(--shades-white-100); --label-default-bg: var(--violet-dark-80); --label-default-color: var(--shades-white-80); --label-grey-bg: var(--shades-black-80); --label-grey-color: var(--shades-white-80); --subchannels-modal-color: var(--shades-black-60); --subchannels-modal-color-hover: var(--shades-white-100); --color-selector-active-bg: var(--shades-black-80); --relationship-modal-color: var(--shades-black-60); --arrow-extension-color: var(--shades-black-60); --modal-close-color: var(--shades-black-60); --modal-close-color-hover: var(--shades-white-100); --add-members-group-header-color: var(--shades-black-60); --add-members-item-color: var(--shades-black-60); --add-members-item-color-hover: var(--shades-white-100); --add-members-item-disabled-color: var(--shades-black-80); --add-members-item-disabled-color-hover: var(--shades-black-60); --add-members-remove-pending-color: var(--error-primary); --add-members-remove-pending-color-hover: var(--error-light-50); --radio-border: var(--shades-black-70); --radio-color: var(--shades-white-60); --notification-settings-option-selected-bg: var(--shades-black-80); --notification-settings-option-title-color: var(--shades-white-90); --notification-settings-option-color: var(--shades-white-60); --notification-settings-option-invalid-color: var(--shades-black-80); --notification-settings-option-invalid-selected-color: var(--shades-black-60); --danger-zone-subheading-color: var(--shades-white-60); --danger-zone-explanation-color: var(--shades-black-60); --thread-creation-search-container-bg: var(--shades-black-90); --thread-creation-close-search-color: var(--shades-black-60); --thread-creation-search-item-bg-hover: var(--shades-black-80); --thread-creation-search-item-info-color: var(--shades-black-60); --chat-message-list-active-border: #5989d6; --sidebars-modal-color: var(--shades-black-60); --sidebars-modal-color-hover: var(--shades-white-100); --inline-engagement-bg: var(--shades-black-70); --inline-engagement-bg-hover: var(--shades-black-80); --inline-engagement-color: var(--fg); --compose-subchannel-header-fg: var(--shades-black-60); --compose-subchannel-header-bg: var(--shades-black-80); --compose-subchannel-label-color: var(--shades-black-60); --compose-subchannel-mark-color: var(--violet-light-100); --enum-option-icon-color: var(--violet-dark-100); --show-password-bg-hover: var(--shades-black-70); --typeahead-overlay-light: var(--shades-black-80); --typeahead-overlay-dark: var(--shades-black-90); --typeahead-overlay-text: var(--shades-white-100); --typeahead-overlay-shadow-primary: rgba(0, 0, 0, 0.25); --typeahead-overlay-shadow-secondary: rgba(0, 0, 0, 0.4); --spoiler-text-color: var(--spoiler-color); --spoiler-background-color: var(--spoiler-color); --purple-link: var(--violet-light-100); --drawer-expand-button: var(--shades-black-60); --drawer-expand-button-disabled: var(--shades-black-80); --drawer-item-color: var(--shades-white-60); --drawer-active-item-color: var(--shades-white-100); --drawer-open-community-bg: #191919; --active-drawer-item-bg: rgba(0, 0, 0, 0.5); --community-drawer-lines: rgba(255, 255, 255, 0.08); --topbar-button-bg: var(--shades-black-90); --filters-button-bg: var(--shades-black-100); --filters-button-border: var(--shades-black-80); --filters-button-hover-bg: var(--shades-black-90); --filter-panel-fg: var(--shades-black-60); --filter-panel-bg: #0d0d0d; --topbar-button-bg-hover: var(--shades-black-80); --topbar-button-fg: var(--shades-white-60); --message-label-color: var(--shades-black-60); --topbar-lines: rgba(255, 255, 255, 0.08); --pin-message-information-text-color: var(--shades-white-60); --pin-message-modal-border-color: var(--shades-black-80); --pinned-count-banner-color: var(--shades-black-90); --pinned-count-text-color: var(--shades-white-90); --modal-overlay-background-90: rgba(0, 0, 0, 0.9); --modal-overlay-background-80: rgba(0, 0, 0, 0.8); --edit-avatar-button: var(--violet-dark-60); --dropdown-text: var(--shades-white-100); --dropdown-select-bg: var(--shades-black-90); --dropdown-select-border: var(--shades-white-60); --dropdown-option-bg: var(--shades-black-80); --dropdown-option-hover-bg: var(--shades-black-70); --dropdown-selected-option-check-color: var(--violet-dark-100); --dropdown-chevron-color: var(--shades-white-100); --dropdown-disabled-color: var(--shades-black-60); --change-member-role-modal-description-text: var(--shades-white-60); --change-member-role-modal-generic-text: var(--shades-white-100); --change-member-role-modal-disabled-background: var(--shades-black-80); --unsaved-changes-modal-text-color: var(--shades-white-60); --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); }