diff --git a/src/components/AssetsLibrary/FilesGridView.jsx b/src/components/AssetsLibrary/FilesGridView.jsx index 2618fab04..1ab6924a4 100644 --- a/src/components/AssetsLibrary/FilesGridView.jsx +++ b/src/components/AssetsLibrary/FilesGridView.jsx @@ -10,6 +10,7 @@ import DeleteFileLinkModal from '../LinksMenu/DeleteFileLinkModal' import EditFileAttachment from '../LinksMenu/EditFileAttachment' import SubFolder from './SubFolder' import ItemOperations from './ItemOperations' +import UserTooltip from '../User/UserTooltip' import FolderIcon from '../../assets/icons/v.2.5/icon-folder-small.svg' @@ -28,6 +29,7 @@ const FilesGridView = ({ title, selectedUsers, onAddAttachment, + assetsMembers, isSharingAttachment, discardAttachments, onChangePermissions, @@ -78,6 +80,7 @@ const FilesGridView = ({ link={ subFolderContent } renderLink={ renderLink } goBack={goBack} + assetsMembers={assetsMembers} onDeletePostAttachment={onDeletePostAttachment} loggedInUser={loggedInUser} formatModifyDate={formatModifyDate} @@ -99,6 +102,7 @@ const FilesGridView = ({
  • Type
    Name
    +
    Created By
    Modified
  • @@ -119,12 +123,22 @@ const FilesGridView = ({ const canEdit = `${link.createdBy}` === `${loggedInUser.userId}` const changeSubFolder = () => onChangeSubFolder(link) + const owner = _.find(assetsMembers, m => m.userId === _.parseInt(link.createdBy)) if (Array.isArray(link.children) && link.children.length > 0) { return (
  • {formatFolderTitle(link.title)}

    +
    + {!owner && (
    Unknown
    )} + {owner && ( +
    +
    + +
    +
    )} +
    {formatModifyDate(link)}
  • ) @@ -161,6 +175,15 @@ const FilesGridView = ({

    {renderLink(link)}

    +
    + {!owner && (
    Unknown
    )} + {owner && ( +
    +
    + +
    +
    )} +
    {formatModifyDate(link)}
    {canEdit && ( diff --git a/src/components/AssetsLibrary/GridView.scss b/src/components/AssetsLibrary/GridView.scss index 7ab34c62b..638539628 100644 --- a/src/components/AssetsLibrary/GridView.scss +++ b/src/components/AssetsLibrary/GridView.scss @@ -85,7 +85,7 @@ } } -.item-modified { +.item-modified, .item-created-by { -webkit-box-flex: 0; -ms-flex: none; flex: none; @@ -134,4 +134,11 @@ .assets-gridview-container-active { position: relative; +} + +:global { + .user-block .tooltip-container { + text-align: left; + line-height: 20px; + } } \ No newline at end of file diff --git a/src/components/AssetsLibrary/LinksGridView.jsx b/src/components/AssetsLibrary/LinksGridView.jsx index 25fd4b45d..b73c5b9ce 100644 --- a/src/components/AssetsLibrary/LinksGridView.jsx +++ b/src/components/AssetsLibrary/LinksGridView.jsx @@ -8,6 +8,7 @@ import DeleteLinkModal from '../LinksMenu/DeleteLinkModal' import EditLinkModal from '../LinksMenu/EditLinkModal' import SubFolder from './SubFolder' import ItemOperations from './ItemOperations' +import UserTooltip from '../User/UserTooltip' import FolderIcon from '../../assets/icons/v.2.5/icon-folder-small.svg' import LinkIcon from '../../assets/icons/link-12.svg' @@ -28,6 +29,7 @@ const LinksGridView = ({ title, formatModifyDate, formatFolderTitle, + assetsMembers, }) => { const renderLink = (link) => { if (link.onClick) { @@ -58,8 +60,9 @@ const LinksGridView = ({ link={ subFolderContent } renderLink={ renderLink } goBack={goBack} - formatModifyDate={formatModifyDate} + assetsMembers={assetsMembers} isLinkSubFolder + formatModifyDate={formatModifyDate} />)} {(!subFolderContent) && (
    = 0 || linkToDelete >= 0)}, '')}> @@ -69,6 +72,7 @@ const LinksGridView = ({
  • Type
    Name
    +
    Created By
    Modified
  • @@ -87,12 +91,24 @@ const LinksGridView = ({ const onEditCancel = () => onEditIntent(-1) const handleEditClick = () => onEditIntent(idx) const changeSubFolder = () => onChangeSubFolder(link) + const owner = _.find(assetsMembers, m => m.userId === _.parseInt(link.createdBy)) if (Array.isArray(link.children) && link.children.length > 0) { return (
  • {formatFolderTitle(link.title)}

    +
    + {!owner && !link.createdBy && (
    )} + {!owner && link.createdBy !== 'CoderBot' && (
    Unknown
    )} + {!owner && link.createdBy === 'CoderBot' && (
    CoderBot
    )} + {owner && ( +
    +
    + +
    +
    )} +
    {formatModifyDate(link)}
  • ) @@ -119,6 +135,16 @@ const LinksGridView = ({
  • {renderLink(link)}

    +
    + {!owner && !link.createdBy && (
    )} + {!owner && link.createdBy && (
    Unknown
    )} + {owner && ( +
    +
    + +
    +
    )} +
    {formatModifyDate(link)}
    {(canEdit || canDelete) && ( diff --git a/src/components/AssetsLibrary/SubFolder.jsx b/src/components/AssetsLibrary/SubFolder.jsx index e8680cbe4..0b0576b91 100644 --- a/src/components/AssetsLibrary/SubFolder.jsx +++ b/src/components/AssetsLibrary/SubFolder.jsx @@ -1,9 +1,11 @@ import React from 'react' import PropTypes from 'prop-types' +import _ from 'lodash' import cn from 'classnames' import DeleteFileLinkModal from '../LinksMenu/DeleteFileLinkModal' import ItemOperations from './ItemOperations' +import UserTooltip from '../User/UserTooltip' import FolderIcon from '../../assets/icons/v.2.5/icon-folder-small.svg' import './GridView.scss' @@ -54,8 +56,9 @@ class SubFolder extends React.Component { } render() { - const { link, renderLink, goBack, formatModifyDate, isLinkSubFolder } = this.props + const { link, renderLink, goBack, formatModifyDate, isLinkSubFolder, assetsMembers } = this.props const { linkToDelete } = this.state + return (
    = 0)}, '')}> {(linkToDelete >= 0) &&
    } @@ -64,17 +67,20 @@ class SubFolder extends React.Component {
  • Type
    Name
    +
    Created By
    Modified
  • ..
    +
  • { link.children.map((childLink, i) => { + const owner = _.find(assetsMembers, m => m.userId === _.parseInt(childLink.createdBy)) if (linkToDelete === i) { return (
  • @@ -100,6 +106,16 @@ class SubFolder extends React.Component { return (
  • {renderLink(childLink)}

    +
    + {!owner && childLink.createdBy !== 'CoderBot' && (
    Unknown
    )} + {!owner && childLink.createdBy === 'CoderBot' && (
    CoderBot
    )} + {owner && ( +
    +
    + +
    +
    )} +
    {formatModifyDate(childLink)}
    {childLink.deletable && this.hasAccess(childLink.createdBy) && ( @@ -123,7 +139,6 @@ SubFolder.propTypes = { onDeletePostAttachment: PropTypes.func, goBack: PropTypes.func.isRequired, loggedInUser: PropTypes.object, - formatModifyDate: PropTypes.func.isRequired, } export default SubFolder diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx index 35b7d6eb4..2d463657b 100644 --- a/src/components/Footer/Footer.jsx +++ b/src/components/Footer/Footer.jsx @@ -10,10 +10,11 @@ require('./Footer.scss') const Footer = () => { const currentYear = moment().format('YYYY') const otherNavigationItems = [ - {img: '', text: 'About', link: 'https://www.topcoder.com/about/', target: '_blank'}, - {img: '', text: 'Contact us', link: 'https://www.topcoder.com/contact/', target: '_blank'}, - {img: '', text: 'Privacy', link: 'https://www.topcoder.com/community/how-it-works/privacy-policy/', target: '_blank'}, - {img: '', text: 'Terms', link: 'https://connect.topcoder.com/terms', target: '_blank'} + {img: '', text: 'About', link: 'https://www.topcoder.com/company/', target: '_blank'}, + {img: '', text: 'Contact us', link: 'https://www.topcoder.com/contact-us/', target: '_blank'}, + {img: '', text: 'Privacy', link: 'https://www.topcoder.com/privacy-policy/', target: '_blank'}, + {img: '', text: 'Terms', link: 'https://connect.topcoder.com/terms', target: '_blank'}, + {img: '', text: 'Our Process', link: 'https://www.topcoder.com/solutions/how-it-works/', target: '_blank'} ] const isProjectDetails = /projects\/\d+/.test(window.location.pathname) const isCreateProject = window.location.pathname.startsWith(NEW_PROJECT_PATH) diff --git a/src/components/FooterV2/FooterV2.jsx b/src/components/FooterV2/FooterV2.jsx index 0c2078f47..da84989d8 100644 --- a/src/components/FooterV2/FooterV2.jsx +++ b/src/components/FooterV2/FooterV2.jsx @@ -5,10 +5,11 @@ import './FooterV2.scss' const FooterV2 = () => (
    © Topcoder { moment().format('YYYY') } diff --git a/src/components/MenuItem/MenuItem.jsx b/src/components/MenuItem/MenuItem.jsx index da3a3297d..2c8bb9233 100644 --- a/src/components/MenuItem/MenuItem.jsx +++ b/src/components/MenuItem/MenuItem.jsx @@ -21,7 +21,8 @@ const MenuItem = ({ isAccordionOpen, onAccordionToggle, match, - wrapperClass + wrapperClass, + toolTipText, }) => { const matchedPath = match && match.path const isChildActive = children && some(children, c => c.to === matchedPath) @@ -47,7 +48,7 @@ const MenuItem = ({ {label} - {!!count && } + {!!count && } )} @@ -68,7 +69,7 @@ const MenuItem = ({ {label} - {!!count && } + {!!count && } @@ -100,7 +101,8 @@ MenuItem.propTypes = { children: PT.array, isAccordionOpen: PT.bool, onAccordionToggle: PT.func, - wrapperClass: PT.string + wrapperClass: PT.string, + toolTipText: PT.string, } export default withRouter(MenuItem) diff --git a/src/components/MenuList/MenuList.jsx b/src/components/MenuList/MenuList.jsx index 7e76ba82a..8eab96ee2 100644 --- a/src/components/MenuList/MenuList.jsx +++ b/src/components/MenuList/MenuList.jsx @@ -21,7 +21,8 @@ MenuList.propTypes = { to: PT.string.isRequired, Icon: PT.func.isRequired, exact: PT.bool, - isActive: PT.func + isActive: PT.func, + toolTipText: PT.string }) ), onAccordionToggle: PT.func.isRequired diff --git a/src/components/MobilePage/MobilePage.scss b/src/components/MobilePage/MobilePage.scss index f9699c7fe..061c1af80 100644 --- a/src/components/MobilePage/MobilePage.scss +++ b/src/components/MobilePage/MobilePage.scss @@ -11,6 +11,14 @@ overflow: hidden; overflow-y: scroll; } + + .mobile-page-gateway { + z-index: 10000; + position: absolute; + top: 0; + left: 0; + width: 100%; + } } .container { diff --git a/src/components/NotificationBadge/NotificationBadge.scss b/src/components/NotificationBadge/NotificationBadge.scss index dd74233c6..00917f818 100644 --- a/src/components/NotificationBadge/NotificationBadge.scss +++ b/src/components/NotificationBadge/NotificationBadge.scss @@ -1,15 +1,16 @@ @import '~tc-ui/src/styles/tc-includes'; .badge { - display: inline-flex; - background: $tc-red-70; - color: white; - @include roboto-bold; - border-radius: 100%; - height: 17px; - line-height: 17px; - justify-content: center; - align-items: center; - width: 17px; - font-size: 11px; -} \ No newline at end of file + display: inline-flex; + background: $tc-red-70; + color: white; + @include roboto-bold; + border-radius: 100%; + line-height: 17px; + justify-content: center; + align-items: center; + min-width: 17px; + min-height: 17px; + padding: 0 2px; + font-size: 11px; +} diff --git a/src/components/ProjectInfo/ProjectInfo.jsx b/src/components/ProjectInfo/ProjectInfo.jsx index 2b8f3e6d6..5de8c8916 100644 --- a/src/components/ProjectInfo/ProjectInfo.jsx +++ b/src/components/ProjectInfo/ProjectInfo.jsx @@ -9,6 +9,8 @@ import MobileExpandable from '../MobileExpandable/MobileExpandable' import MediaQuery from 'react-responsive' import { SCREEN_BREAKPOINT_MD, PROJECT_STATUS_ACTIVE, PHASE_STATUS_ACTIVE, PHASE_STATUS_REVIEWED, PROJECT_ROLE_OWNER, PROJECT_ROLE_CUSTOMER } from '../../config/constants' import ReviewProjectButton from '../../projects/detail/components/ReviewProjectButton' +import Tooltip from 'appirio-tech-react-components/components/Tooltip/Tooltip' +import { TOOLTIP_DEFAULT_DELAY } from '../../config/constants' import './ProjectInfo.scss' @@ -51,24 +53,39 @@ class ProjectInfo extends Component { return (
    -
    -
    {_.unescape(project.name)}
    -
    Created {moment(project.createdAt).format('MMM DD, YYYY')}
    - {!!code &&
    {_.unescape(code)}
    } +
    {_.unescape(project.name)}
    + +
    +
    + Created {moment(project.createdAt).format('MMM DD, YYYY')} +
    + {!!code && +
    + +
    +
    {_.unescape(code)}
    +
    +
    + {_.unescape(code)} +
    +
    +
    + + }
    - {showDeleteConfirm && + {showDeleteConfirm && ( - } + )} {showReviewBtn ? ( reviewButtonSection ) : ( - {(matches) => ( + {matches => ( { if (input && ! this.wrapper){this.wrapper = input; this.setState({showUp:this.shouldDropdownUp()}) }}} onClick={e => e.stopPropagation()} > diff --git a/src/components/ProjectStatus/ProjectStatusChangeConfirmation.scss b/src/components/ProjectStatus/ProjectStatusChangeConfirmation.scss index 8fd19c05e..148398bad 100644 --- a/src/components/ProjectStatus/ProjectStatusChangeConfirmation.scss +++ b/src/components/ProjectStatus/ProjectStatusChangeConfirmation.scss @@ -10,7 +10,6 @@ z-index: 20; &.modal{ - position: absolute; } .cancellation-reason { @@ -35,11 +34,11 @@ width: 100%; } } - + &.dropdown-up { top: auto; bottom: 320px; } } } - \ No newline at end of file + diff --git a/src/components/SelectDropdown/SelectDropdown.jsx b/src/components/SelectDropdown/SelectDropdown.jsx index de6ee6e80..9f4695aa6 100644 --- a/src/components/SelectDropdown/SelectDropdown.jsx +++ b/src/components/SelectDropdown/SelectDropdown.jsx @@ -113,7 +113,7 @@ class SelectDropdown extends Component {
  • ) return option.toolTipMessage ? ( - +
    {selectItem}
    diff --git a/src/components/TeamManagement/AutocompleteInputContainer.jsx b/src/components/TeamManagement/AutocompleteInputContainer.jsx index 510ae63c7..db200e09d 100644 --- a/src/components/TeamManagement/AutocompleteInputContainer.jsx +++ b/src/components/TeamManagement/AutocompleteInputContainer.jsx @@ -11,9 +11,25 @@ class AutocompleteInputContainer extends React.Component { constructor(props) { super(props) this.debounceTimer = null + + this.clearUserSuggestions = this.clearUserSuggestions.bind(this) + } + + /** + * Clear user suggestion list + */ + clearUserSuggestions() { + const { currentUser } = this.props + + if (!currentUser.isCustomer) { + // When customer user is typing a user handle to invite we should not try to clear suggestions, + // because we don't show suggestions for customer + this.props.onClearUserSuggestions() + } } onInputChange(inputValue) { + const { currentUser } = this.props const indexOfSpace = inputValue.indexOf(' ') const indexOfSemiColon = inputValue.indexOf(';') @@ -25,15 +41,18 @@ class AutocompleteInputContainer extends React.Component { if (indexOfSpace >= 1 || indexOfSemiColon >= 1 ) { inputValue = inputValue.substring(0, inputValue.length -1 ) this.onUpdate([...this.props.selectedMembers, {label: inputValue, value: inputValue}]) - this.props.onClearUserSuggestions() + this.clearUserSuggestions() // this is return empty to nullify inputValue post processing return '' } if (inputValue.length >= AUTOCOMPLETE_TRIGGER_LENGTH) { - this.props.onLoadUserSuggestions(inputValue) + // When customer user is typing a user handle to invite we should not try to show suggestions as we always get error 403 + if (!currentUser.isCustomer) { + this.props.onLoadUserSuggestions(inputValue) + } } else { - this.props.onClearUserSuggestions() + this.clearUserSuggestions() } } @@ -46,7 +65,7 @@ class AutocompleteInputContainer extends React.Component { if (this.props.onUpdate) { this.props.onUpdate(inputValueNormalized) } - this.props.onClearUserSuggestions() + this.clearUserSuggestions() } render() { diff --git a/src/components/TeamManagement/ProjectManagementDialog.js b/src/components/TeamManagement/ProjectManagementDialog.js index 6cea633f7..aa57bbf7d 100644 --- a/src/components/TeamManagement/ProjectManagementDialog.js +++ b/src/components/TeamManagement/ProjectManagementDialog.js @@ -189,7 +189,7 @@ class ProjectManagementDialog extends React.Component {
    invite more people
    member.userId === currentUser.userId)[0] const modalActive = isAddingTeamMember || deletingMember || isShowJoin || showNewMemberConfirmation || deletingInvite - const customerTeamManageAction = (currentUser.isCustomer || currentUser.isAdmin) || - (currentMember && currentUser.isManager) - const customerTeamViewAction = !customerTeamManageAction const topcoderTeamManageAction = currentUser.isAdmin || (currentMember && checkPermission(PERMISSIONS.INVITE_TOPCODER_MEMBER)) const topcoderTeamViewAction = !topcoderTeamManageAction const canJoinAsCopilot = !currentMember && currentUser.isCopilot @@ -80,11 +77,6 @@ class TeamManagement extends React.Component {
    Team - {(customerTeamManageAction || customerTeamViewAction) && - onShowProjectDialog(true)}> - {customerTeamViewAction ? 'View' : 'Manage'} - - }
    {sortedMembers.map((member, i) => { @@ -113,7 +105,7 @@ class TeamManagement extends React.Component { { (canShowInvite) &&
    onShowProjectDialog(true)}> - Invite people + Manage Invitations
    }
    diff --git a/src/components/TopBar/ProjectsToolBar.js b/src/components/TopBar/ProjectsToolBar.js index dd16a5585..909e578b2 100644 --- a/src/components/TopBar/ProjectsToolBar.js +++ b/src/components/TopBar/ProjectsToolBar.js @@ -7,7 +7,6 @@ import { withRouter, Prompt } from 'react-router-dom' import { connect } from 'react-redux' import _ from 'lodash' import SearchBar from 'appirio-tech-react-components/components/SearchBar/SearchBar' -import MenuBar from 'appirio-tech-react-components/components/MenuBar/MenuBar' import NotificationsDropdown from '../NotificationsDropdown/NotificationsDropdownContainer' import NewProjectNavLink from './NewProjectNavLink' import MobileMenu from '../MobileMenu/MobileMenu' @@ -151,26 +150,6 @@ class ProjectsToolBar extends Component { const onLeaveMessage = this.onLeave() || '' - const primaryNavigationItems = [ - { - text: 'My Projects', - link: '/projects' - }, - { - text: 'Getting Started', - link: 'https://www.topcoder.com/about-topcoder/connect/', - target: '_blank', - absolute: true - }, - { - text: 'Help', - link: 'https://help.topcoder.com/hc/en-us/articles/225540188-Topcoder-Connect-FAQs', - target: '_blank', - absolute: true - } - ] - const menuBar = isLoggedIn && !isPowerUser && - return (
    - { renderLogoSection(menuBar) } + { renderLogoSection() } { isLoggedIn && !isPowerUser &&
    MY PROJECTS
    } { isLoggedIn && !!isPowerUser && diff --git a/src/components/TopBar/TopBarContainer.js b/src/components/TopBar/TopBarContainer.js index 9165500d6..b3ad647ee 100644 --- a/src/components/TopBar/TopBarContainer.js +++ b/src/components/TopBar/TopBarContainer.js @@ -86,9 +86,6 @@ class TopBarContainer extends React.Component { { label: 'Account and security', link: '/settings/account' }, { label: 'Notification settings', link: '/settings/notifications' }, ], - [ - { label: 'Help', link: 'https://help.topcoder.com/hc/en-us', absolute: true, id: 0 } - ], [ { label: 'Log out', onClick: logoutClick, absolute: true, id: 0 } ] @@ -102,13 +99,12 @@ class TopBarContainer extends React.Component { { label: 'My profile', link: '/settings/profile' }, { label: 'Account and security', link: '/settings/account' }, { label: 'Notification settings', link: '/settings/notifications' }, - { label: 'Help', link: 'https://help.topcoder.com/hc/en-us', absolute: true }, ] }, { items: [ - { label: 'About', link: 'https://www.topcoder.com/about/', absolute: true }, - { label: 'Contact us', link: 'https://www.topcoder.com/contact/', absolute: true }, - { label: 'Privacy', link: 'https://www.topcoder.com/community/how-it-works/privacy-policy/', absolute: true }, + { label: 'About', link: 'https://www.topcoder.com/company/', absolute: true }, + { label: 'Contact us', link: 'https://www.topcoder.com/contact-us/', absolute: true }, + { label: 'Privacy', link: 'https://www.topcoder.com/privacy-policy/', absolute: true }, { label: 'Terms', link: 'https://connect.topcoder.com/terms', absolute: true }, ] }, { diff --git a/src/components/TopicDrawer/TopicDrawer.jsx b/src/components/TopicDrawer/TopicDrawer.jsx index bc2d19265..8977f72c5 100644 --- a/src/components/TopicDrawer/TopicDrawer.jsx +++ b/src/components/TopicDrawer/TopicDrawer.jsx @@ -4,6 +4,8 @@ import Drawer from 'appirio-tech-react-components/components/Drawer/Drawer' import FeedContainer from '../../projects/detail/containers/FeedContainer' +import './TopicDrawer.scss' + /** * A drawer that shows the posts under the selected topic */ @@ -18,6 +20,7 @@ const TopicDrawer = ({ return ( div:nth-child(2) { + @media screen and (max-width: $screen-md - 1px) { + width: 100% !important; + top: 0 !important; + height: 100% !important; + } + } +} diff --git a/src/projects/detail/components/PhaseCard/EditStageForm.jsx b/src/projects/detail/components/PhaseCard/EditStageForm.jsx index aa7b15ac3..3b9efb76a 100644 --- a/src/projects/detail/components/PhaseCard/EditStageForm.jsx +++ b/src/projects/detail/components/PhaseCard/EditStageForm.jsx @@ -20,6 +20,10 @@ import Tooltip from 'appirio-tech-react-components/components/Tooltip/Tooltip' import { TOOLTIP_DEFAULT_DELAY } from '../../../../config/constants' import { getPhaseActualData } from '../../../../helpers/projectHelper' import DeletePhase from './DeletePhase' +import { + ROLE_CONNECT_ADMIN, + ROLE_ADMINISTRATOR, +} from '../../../../config/constants' const moment = extendMoment(Moment) const phaseStatuses = PHASE_STATUS.map(ps => ({ @@ -33,7 +37,7 @@ class EditStageForm extends React.Component { this.state = { isUpdating: false, - isEdittable: _.get(props, 'phase.status') !== PHASE_STATUS_COMPLETED, + isEdittable: (_.get(props, 'phase.status') !== PHASE_STATUS_COMPLETED) || (_.get(props, 'isAdmin')), disableActiveStatusFields: _.get(props, 'phase.status') !== PHASE_STATUS_ACTIVE, showPhaseOverlapWarning: false, phaseIsdirty: false, @@ -83,7 +87,7 @@ class EditStageForm extends React.Component { componentWillReceiveProps(nextProps) { this.setState({ isUpdating: nextProps.isUpdating, - isEdittable: nextProps.phase.status !== PHASE_STATUS_COMPLETED, + isEdittable: nextProps.phase.status !== PHASE_STATUS_COMPLETED || nextProps.isAdmin, disableActiveStatusFields: nextProps.phase.status !== PHASE_STATUS_ACTIVE, }) @@ -399,11 +403,18 @@ EditStageForm.propTypes = { phaseIndex: PT.number } -const mapStateToProps = ({projectState, productsTimelines}) => ({ - isUpdating: projectState.processing, - phases: projectState.phases, - productsTimelines -}) +const mapStateToProps = ({projectState, productsTimelines, loadUser}) => { + const adminRoles = [ + ROLE_ADMINISTRATOR, + ROLE_CONNECT_ADMIN, + ] + return { + isUpdating: projectState.processing, + phases: projectState.phases, + productsTimelines, + isAdmin: _.intersection(loadUser.user.roles, adminRoles).length > 0 + } +} const actionCreators = { updatePhaseAction, diff --git a/src/projects/detail/components/PhaseCard/PhaseCard.jsx b/src/projects/detail/components/PhaseCard/PhaseCard.jsx index 042af03a0..2f69c824a 100644 --- a/src/projects/detail/components/PhaseCard/PhaseCard.jsx +++ b/src/projects/detail/components/PhaseCard/PhaseCard.jsx @@ -19,6 +19,8 @@ import { PROJECT_STATUS_CANCELLED, SCREEN_BREAKPOINT_MD, EVENT_TYPE, + ROLE_CONNECT_ADMIN, + ROLE_ADMINISTRATOR, } from '../../../../config/constants' import ProjectProgress from '../../../../components/ProjectProgress/ProjectProgress' @@ -93,7 +95,8 @@ class PhaseCard extends React.Component { hasUnseen, phaseId, isExpanded, - project + project, + isAdmin } = this.props const progressInPercent = attr.progressInPercent || 0 @@ -101,7 +104,7 @@ class PhaseCard extends React.Component { status = _.find(PHASE_STATUS, s => s.value === status) ? status : PHASE_STATUS_DRAFT const statusDetails = _.find(PHASE_STATUS, s => s.value === status) - const phaseEditable = checkPermission(PERMISSIONS.EDIT_PROJECT_PLAN, project) && status !== PHASE_STATUS_COMPLETED && projectStatus !== PROJECT_STATUS_CANCELLED && projectStatus !== PROJECT_STATUS_COMPLETED + const phaseEditable = checkPermission(PERMISSIONS.EDIT_PROJECT_PLAN, project) && (status !== PHASE_STATUS_COMPLETED || (isAdmin)) && projectStatus !== PROJECT_STATUS_CANCELLED && projectStatus !== PROJECT_STATUS_COMPLETED return (
    @@ -280,9 +283,14 @@ PhaseCard.propTypes = { const mapStateToProps = ({loadUser, projectState}) => { + const adminRoles = [ + ROLE_ADMINISTRATOR, + ROLE_CONNECT_ADMIN, + ] return { currentUserRoles: loadUser.user.roles, - isUpdating: projectState.processing + isUpdating: projectState.processing, + isAdmin: _.intersection(loadUser.user.roles, adminRoles).length > 0 } } diff --git a/src/projects/detail/components/timeline/FormFieldDate/FormFieldDate.jsx b/src/projects/detail/components/timeline/FormFieldDate/FormFieldDate.jsx index 153b649f4..c62b5030d 100644 --- a/src/projects/detail/components/timeline/FormFieldDate/FormFieldDate.jsx +++ b/src/projects/detail/components/timeline/FormFieldDate/FormFieldDate.jsx @@ -5,32 +5,21 @@ */ import React from 'react' import PT from 'prop-types' -import _ from 'lodash' -import cn from 'classnames' import FormsyForm from 'appirio-tech-react-components/components/Formsy' +import './FormFieldDate.scss' const TCFormFields = FormsyForm.Fields -import styles from './FormFieldDate.scss' -const FormFieldDate = ({ startDate, endDate, theme }) => { - const startDateProps = _.omit(startDate, 'label') - startDateProps.type = 'date' - startDateProps.wrapperClass = styles['field-wrapper'] - - const endDateProps = _.omit(endDate, 'label') - endDateProps.type = 'date' - endDateProps.wrapperClass = styles['field-wrapper'] +const FormFieldDate = (props) => { return ( -
    +
    - +
    - - - +
    ) diff --git a/src/projects/detail/components/timeline/Milestone/Milestone.jsx b/src/projects/detail/components/timeline/Milestone/Milestone.jsx index 3d092febe..c9ea78c9a 100644 --- a/src/projects/detail/components/timeline/Milestone/Milestone.jsx +++ b/src/projects/detail/components/timeline/Milestone/Milestone.jsx @@ -102,10 +102,32 @@ class Milestone extends React.Component { this.setState({ isMobileEditing: true }) } + isActualStartDateEditable() { + const { milestone, currentUser } = this.props + const isActive = milestone.status === MILESTONE_STATUS.ACTIVE + const isCompleted = milestone.status === MILESTONE_STATUS.COMPLETED + return (isActive || isCompleted) && currentUser.isAdmin + + } + + isCompletionDateEditable() { + const { milestone, currentUser } = this.props + const isCompleted = milestone.status === MILESTONE_STATUS.COMPLETED + return isCompleted && currentUser.isAdmin + } + updateMilestoneWithData(values) { const { milestone, updateMilestone } = this.props - - updateMilestone(milestone.id, values) + const milestoneData = { + ...values + } + if (values.actualStartDate) { + milestoneData.actualStartDate = moment.utc(new Date(values.actualStartDate)) + } + if (values.completionDate) { + milestoneData.completionDate = moment.utc(new Date(values.completionDate)) + } + updateMilestone(milestone.id, milestoneData) } milestoneEditorChanged(values) { @@ -211,6 +233,8 @@ class Milestone extends React.Component { const date = startDate.format('D') const title = milestone.name const isUpdating = milestone.isUpdating + const isActualDateEditable = this.isActualStartDateEditable() + const isCompletionDateEditable = this.isCompletionDateEditable() const editForm = (
    ]+href="(.*?)"[^>]*>([\s\S]*?)<\/a>/gm const urlRegex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/gm // eslint-disable-line no-useless-escape - const rawLinks = regex.exec(str) + const rawLinks = regex.exec(post.rawContent) if (Array.isArray(rawLinks)) { let i = 0 @@ -238,7 +248,10 @@ class AssetsInfoContainer extends React.Component { if (urlRegex.test(address)) { links.push({ title, - address + address, + // use created at `date` to show as modified time for links inside folders + updatedAt: post.date, + createdBy: post.userId }) } @@ -249,11 +262,11 @@ class AssetsInfoContainer extends React.Component { return links } - extractMarkdownLink(str) { + extractMarkdownLink(post) { const links = [] const regex = /(?:__|[*#])|\[(.*?)\]\((.*?)\)/gm const urlRegex = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/gm // eslint-disable-line no-useless-escape - const rawLinks = regex.exec(str) + const rawLinks = regex.exec(post.rawContent) if (Array.isArray(rawLinks)) { let i = 0 @@ -264,7 +277,10 @@ class AssetsInfoContainer extends React.Component { if (urlRegex.test(address)) { links.push({ title, - address + address, + // use created at `date` to show as modified time for links inside folders + updatedAt: post.date, + createdBy: post.userId }) } @@ -275,10 +291,10 @@ class AssetsInfoContainer extends React.Component { return links } - extractRawLink(str) { + extractRawLink(post) { let links = [] const regex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,}[\s])(\s|$)/igm // eslint-disable-line no-useless-escape - const rawLinks = str.match(regex) + const rawLinks = post.rawContent.match(regex) if (Array.isArray(rawLinks)) { links = rawLinks @@ -289,7 +305,10 @@ class AssetsInfoContainer extends React.Component { return { title: name, - address: url + address: url, + // use created at `date` to show as modified time for links inside folders + updatedAt: post.date, + createdBy: post.userId } }) } @@ -303,15 +322,18 @@ class AssetsInfoContainer extends React.Component { let childrenLinks = [] feed.posts.forEach(post => { childrenLinks = childrenLinks.concat([ - ...this.extractHtmlLink(post.rawContent), - ...this.extractMarkdownLink(post.rawContent), - ...this.extractRawLink(post.rawContent) + ...this.extractHtmlLink(post), + ...this.extractMarkdownLink(post), + ...this.extractRawLink(post) ]) }) if (childrenLinks.length > 0) { links.push({ title: feed.title, + createdBy: feed.userId, + // use created at `date` to show as modified time for folders + updatedAt: feed.date, children: childrenLinks }) } @@ -336,6 +358,7 @@ class AssetsInfoContainer extends React.Component { attachmentId: attachment.id, attachment: true, deletable: true, + createdAt: post.date, createdBy: attachment.createdBy, postId: post.id, topicId: feed.id, @@ -348,6 +371,9 @@ class AssetsInfoContainer extends React.Component { if (attachmentLinksPerFeed.length > 0) { attachmentLinks.push({ title: feed.title, + createdBy: feed.userId, + // use created at `date` to show as modified time for folders + updatedAt: feed.date, children: attachmentLinksPerFeed }) } @@ -382,22 +408,9 @@ class AssetsInfoContainer extends React.Component { } } - render() { - const { project, currentMemberRole, isSuperUser, phases, feeds, - isManageUser, phasesTopics, projectTemplates, hideLinks, - attachmentsAwaitingPermission, addProjectAttachment, discardAttachments, attachmentPermissions, - changeAttachmentPermission, projectMembers, loggedInUser, isSharingAttachment, canAccessPrivatePosts } = this.props - const { ifModalOpen } = this.state - - const canManageLinks = !!currentMemberRole || isSuperUser - - let devices = [] - const primaryTarget = _.get(project, 'details.appDefinition.primaryTarget') - if (primaryTarget && !primaryTarget.seeAttached) { - devices.push(primaryTarget.value) - } else { - devices = _.get(project, 'details.devices', []) - } + getLinksAndAttachments() { + const { project, isSuperUser, phases, feeds, + isManageUser, phasesTopics, canAccessPrivatePosts } = this.props let attachments = project.attachments // merges the product attachments to show in the links menu @@ -444,16 +457,6 @@ class AssetsInfoContainer extends React.Component { }) ) - const attachmentsStorePath = `${PROJECT_ATTACHMENTS_FOLDER}/${project.id}/` - let enableFileUpload = true - if(project.version !== 'v2') { - const templateId = _.get(project, 'templateId') - const projectTemplate = _.find(projectTemplates, template => template.id === templateId) - enableFileUpload = _.some(projectTemplate.scope.sections, section => { - return _.some(section.subSections, subSection => subSection.id === 'files') - }) - } - // extract links from posts const topicLinks = this.extractLinksFromPosts(feeds) const publicTopicLinks = topicLinks.filter(link => link.tag !== PROJECT_FEED_TYPE_MESSAGES) @@ -475,6 +478,65 @@ class AssetsInfoContainer extends React.Component { ...this.extractAttachmentLinksFromPosts(phaseFeeds) ] + return ({ + links, + attachments, + }) + } + + componentDidMount() { + const {loadMembers } = this.props + const {links, attachments} = this.getLinksAndAttachments() + + let tmpUserIds = [] + let userIds = [] + _.forEach(links, link => { + tmpUserIds = _.union(tmpUserIds, _.map(link.children, 'createdBy')) + tmpUserIds = _.union(tmpUserIds, [link.createdBy]) + tmpUserIds = _.union(tmpUserIds, [link.updatedBy]) + }) + + _.forEach(attachments, attachment => { + tmpUserIds = _.union(tmpUserIds, _.map(attachment.children, 'createdBy')) + tmpUserIds = _.union(tmpUserIds, [attachment.createdBy]) + }) + + _.forEach(tmpUserIds, userId => { + userIds = _.union(userIds, [_.parseInt(userId)]) + }) + _.remove(userIds, i => !i) + + loadMembers(userIds) + } + + render() { + const { project, currentMemberRole, isSuperUser, projectTemplates, hideLinks, + attachmentsAwaitingPermission, addProjectAttachment, discardAttachments, attachmentPermissions, + changeAttachmentPermission, projectMembers, loggedInUser, isSharingAttachment, assetsMembers } = this.props + const { ifModalOpen } = this.state + + const canManageLinks = !!currentMemberRole || isSuperUser + + let devices = [] + const primaryTarget = _.get(project, 'details.appDefinition.primaryTarget') + if (primaryTarget && !primaryTarget.seeAttached) { + devices.push(primaryTarget.value) + } else { + devices = _.get(project, 'details.devices', []) + } + + const {links, attachments} = this.getLinksAndAttachments() + + const attachmentsStorePath = `${PROJECT_ATTACHMENTS_FOLDER}/${project.id}/` + let enableFileUpload = true + if(project.version !== 'v2') { + const templateId = _.get(project, 'templateId') + const projectTemplate = _.find(projectTemplates, template => template.id === templateId) + enableFileUpload = _.some(projectTemplate.scope.sections, section => { + return _.some(section.subSections, subSection => subSection.id === 'files') + }) + } + const assetsData = [] enableFileUpload && assetsData.push({name: 'Files', total: _.toString(attachments.length)}) !hideLinks && assetsData.push({name: 'Links', total: _.toString(links.length)}) @@ -597,6 +659,7 @@ class AssetsInfoContainer extends React.Component { onChangePermissions={changeAttachmentPermission} selectedUsers={attachmentPermissions} projectMembers={projectMembers} + assetsMembers={assetsMembers} pendingAttachments={attachmentsAwaitingPermission} loggedInUser={loggedInUser} attachmentsStorePath={attachmentsStorePath} @@ -607,6 +670,7 @@ class AssetsInfoContainer extends React.Component { {(!hideLinks && activeAssetsType === 'Links') && { attachmentPermissions: projectState.attachmentPermissions, isSharingAttachment: projectState.processingAttachments, projectMembers: _.keyBy(projectMembers, 'userId'), + assetsMembers: _.keyBy(members.members, 'userId'), loggedInUser: loadUser.user, canAccessPrivatePosts }) } -const mapDispatchToProps = { updateProject, deleteProject, addProjectAttachment, updateProjectAttachment, +const mapDispatchToProps = { updateProject, deleteProject, loadMembers, addProjectAttachment, updateProjectAttachment, loadProjectMessages, discardAttachments, uploadProjectAttachments, loadDashboardFeeds, loadTopic, changeAttachmentPermission, removeProjectAttachment, loadProjectPlan, saveFeedComment } diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js index e7cb68676..2b2de373b 100644 --- a/src/projects/detail/containers/ProjectInfoContainer.js +++ b/src/projects/detail/containers/ProjectInfoContainer.js @@ -34,6 +34,7 @@ import { filterNotificationsByProjectId, filterReadNotifications, filterTopicAndPostChangedNotifications, + filterFileAndLinkChangedNotifications, } from '../../../routes/notifications/helpers/notifications' const EnhancedProjectStatus = editableProjectStatus(ProjectStatus) @@ -439,13 +440,20 @@ class ProjectInfoContainer extends React.Component { const projectNotReadNotifications = filterReadNotifications(filterNotificationsByProjectId(notifications, project.id)) const notReadMessageNotifications = filterTopicAndPostChangedNotifications(projectNotReadNotifications, /^(?:MESSAGES|PRIMARY)$/) const notReadPhaseNotifications = filterTopicAndPostChangedNotifications(projectNotReadNotifications, /^phase#\d+$/) + const notReadAssetsNotifications = filterFileAndLinkChangedNotifications(projectNotReadNotifications) const navLinks = getProjectNavLinks(project, project.id).map((navLink) => { if (navLink.label === 'Messages') { navLink.count = notReadMessageNotifications.length + navLink.toolTipText = 'New messages' } if (navLink.label === 'Dashboard') { navLink.count = notReadPhaseNotifications.length + navLink.toolTipText = 'New comments' + } + if (navLink.label === 'Assets Library') { + navLink.count = notReadAssetsNotifications.length + navLink.toolTipText = 'New files and links' } return navLink diff --git a/src/projects/list/components/Projects/ProjectsGridView.jsx b/src/projects/list/components/Projects/ProjectsGridView.jsx index 5727f721b..daafe35b4 100644 --- a/src/projects/list/components/Projects/ProjectsGridView.jsx +++ b/src/projects/list/components/Projects/ProjectsGridView.jsx @@ -6,7 +6,8 @@ import moment from 'moment' import { filterReadNotifications, filterNotificationsByProjectId, - filterTopicAndPostChangedNotifications + filterTopicAndPostChangedNotifications, + filterFileAndLinkChangedNotifications, } from '../../../../routes/notifications/helpers/notifications' import ProjectListTimeSortColHeader from './ProjectListTimeSortColHeader' import ProjectListFilterColHeader from './ProjectListFilterColHeader' @@ -14,7 +15,7 @@ import GridView from '../../../../components/Grid/GridView' import UserTooltip from '../../../../components/User/UserTooltip' import { PROJECTS_LIST_PER_PAGE, SORT_OPTIONS, PROJECT_STATUS_COMPLETED, DATE_TO_USER_FIELD_MAP, PHASE_STATUS_REVIEWED, - PHASE_STATUS_ACTIVE, PROJECT_STATUS_ACTIVE + PHASE_STATUS_ACTIVE, PROJECT_STATUS_ACTIVE, TOOLTIP_DEFAULT_DELAY } from '../../../../config/constants' import { getProjectTemplateByKey } from '../../../../helpers/templates' import TextTruncate from 'react-text-truncate' @@ -23,6 +24,7 @@ import editableProjectStatus from '../../../../components/ProjectStatus/editable import ProjectManagerAvatars from './ProjectManagerAvatars' import ProjectTypeIcon from '../../../../components/ProjectTypeIcon' import IconProjectStatusTitle from '../../../../assets/icons/status-ico.svg' +import Tooltip from 'appirio-tech-react-components/components/Tooltip/Tooltip' import './ProjectsGridView.scss' import NotificationBadge from '../../../../components/NotificationBadge/NotificationBadge' @@ -87,7 +89,8 @@ const ProjectsGridView = props => { const unreadProjectUpdate = filterNotificationsByProjectId(notReadNotifications, item.id) // count unread posts for Messages tab and Phases const unreadPosts = filterTopicAndPostChangedNotifications(unreadProjectUpdate, /^(?:MESSAGES|PRIMARY|phase#\d+)$/) - const unreadPostsCount = unreadPosts.length + const unreadAssets = filterFileAndLinkChangedNotifications(unreadProjectUpdate) + const unreadCount = unreadPosts.length + unreadAssets.length const recentlyCreated = moment().diff(item.createdAt, 'seconds') < 3600 return (
    @@ -95,7 +98,7 @@ const ProjectsGridView = props => {
    {_.unescape(item.name)} - { unreadPostsCount > 0 && } + { unreadCount > 0 && }
    @@ -118,7 +121,16 @@ const ProjectsGridView = props => { const code = _.get(item, 'details.utm.code', '') return (
    - {code} +
    + +
    + {code} +
    +
    + {code} +
    +
    +
    ) } diff --git a/src/projects/list/components/Projects/ProjectsGridView.scss b/src/projects/list/components/Projects/ProjectsGridView.scss index e4987fa74..6cdf0880b 100644 --- a/src/projects/list/components/Projects/ProjectsGridView.scss +++ b/src/projects/list/components/Projects/ProjectsGridView.scss @@ -26,6 +26,42 @@ $screen-one-column: 720px; .item-ref-code { flex: 1 3 100px; min-width: 100px; + + .time-container { + display: flex; + width: 100%; + padding-right: 10px; + + .code-tooltip-container { + display: flex; + width: 100%; + + .Tooltip { + max-width: 100%; + + .tooltip-target { + display: flex; + } + + .tooltip-body { + display: flex; + + span { + width: 200px; + white-space: normal; + display: flex; + justify-content: center; + } + } + } + } + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } } .item-status-date { @@ -182,7 +218,6 @@ $screen-one-column: 720px; align-items: baseline; .project-container { - overflow: hidden; .project-title { display: flex; diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index 6abe03f07..e7917dad3 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -40,6 +40,7 @@ const initialState = { project: { invites: [] // invites are pushed directly into it hence need to declare first }, + assetsMembers: {}, projectNonDirty: {}, updateExisting: false, phases: null, @@ -542,6 +543,7 @@ export const projectState = function (state=initialState, action) { phasesNonDirty: updateProductInPhases(state.phasesNonDirty, phaseId, productId, { attachments: { $push: [_.cloneDeep(attachment)] } }), + attachmentsAwaitingPermission: null } } diff --git a/src/reducers/templates.js b/src/reducers/templates.js index b1e74c96d..1589787b0 100644 --- a/src/reducers/templates.js +++ b/src/reducers/templates.js @@ -164,15 +164,13 @@ export default function(state = initialState, action) { Alert.error(`PROJECT METADATA CREATE FAILED: ${action.payload.response.data.result.content.message}`) return { ...state, - isLoading: false, - error: action.payload.response.data.result.content.message + isLoading: false } case UPDATE_PROJECTS_METADATA_FAILURE: Alert.error(`PROJECT METADATA UPDATE FAILED: ${action.payload.response.data.result.content.message}`) return { ...state, - isLoading: false, - error: action.payload.response.data.result.content.message + isLoading: false } case REMOVE_PROJECTS_METADATA_FAILURE: case REMOVE_PRODUCT_CATEGORY_FAILURE: @@ -182,8 +180,7 @@ export default function(state = initialState, action) { Alert.error(`PROJECT METADATA DELETE FAILED: ${action.payload.response.data.result.content.message}`) return { ...state, - isRemoving: false, - error: action.payload.response.data.result.content.message + isRemoving: false } case ADD_PROJECTS_METADATA_SUCCESS: case CREATE_PROJECT_TEMPLATE_SUCCESS: diff --git a/src/routes/metadata/components/MetaDataPanel.jsx b/src/routes/metadata/components/MetaDataPanel.jsx index a71114824..0136ea035 100644 --- a/src/routes/metadata/components/MetaDataPanel.jsx +++ b/src/routes/metadata/components/MetaDataPanel.jsx @@ -871,27 +871,29 @@ class MetaDataPanel extends React.Component {
    ) } - { !isFullScreen && !templates.isLoading && !templates.metadataRevisionsLoading && (!!metadata || isNew ) && ( - ) + { !isFullScreen && !templates.metadataRevisionsLoading && (!!metadata || isNew ) && ( +
    + +
    ) }
    diff --git a/src/routes/metadata/components/MetaDataPanel.scss b/src/routes/metadata/components/MetaDataPanel.scss index 9bfd96569..cff4fd7db 100644 --- a/src/routes/metadata/components/MetaDataPanel.scss +++ b/src/routes/metadata/components/MetaDataPanel.scss @@ -37,7 +37,7 @@ .ProjectWizard { padding: 10px; - + .section-footer { margin-top: 0px;// resets the negative margin which is used in actual rendering } @@ -242,4 +242,8 @@ } } } + + .hide { + display: none; + } } diff --git a/src/routes/metadata/containers/FormDetails.jsx b/src/routes/metadata/containers/FormDetails.jsx index fdec345e3..daa1f15e7 100644 --- a/src/routes/metadata/containers/FormDetails.jsx +++ b/src/routes/metadata/containers/FormDetails.jsx @@ -16,6 +16,7 @@ import { getRevisionList, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -68,6 +69,7 @@ class FormDetails extends React.Component { templates, isAdmin, match, + isLoading } = this.props const key = match.params.key let form @@ -76,20 +78,23 @@ class FormDetails extends React.Component { } return (
    - + {isLoading && ()} +
    + +
    ) } @@ -138,7 +143,14 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving && !props.versionOptionsLoading) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.versionMetadata)) && + !props.isRemoving && + !props.versionOptionsLoading +) const FormDetailsWithLoaderEnhanced = enhance(errorHandler(FormDetails)) const FormDetailsWithLoaderAndAuth = requiresAuthentication(FormDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/MetaDataContainer.scss b/src/routes/metadata/containers/MetaDataContainer.scss index 65a84c07b..fcf6553c0 100644 --- a/src/routes/metadata/containers/MetaDataContainer.scss +++ b/src/routes/metadata/containers/MetaDataContainer.scss @@ -45,4 +45,8 @@ } } } + + .hide { + display: none; + } } diff --git a/src/routes/metadata/containers/MilestoneTemplateDetails.jsx b/src/routes/metadata/containers/MilestoneTemplateDetails.jsx index 8840b5bf9..96d9d6f18 100644 --- a/src/routes/metadata/containers/MilestoneTemplateDetails.jsx +++ b/src/routes/metadata/containers/MilestoneTemplateDetails.jsx @@ -13,6 +13,7 @@ import { updateProjectsMetadata, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -43,7 +44,7 @@ class MilestoneTemplateDetails extends React.Component { createMilestoneTemplate, updateProjectsMetadata, templates, - // isLoading, + isLoading, isAdmin, match, } = this.props @@ -52,17 +53,20 @@ class MilestoneTemplateDetails extends React.Component { const milestoneTemplate = _.find(milestoneTemplates, t => String(t.id) === id) return (
    - + {isLoading && ()} +
    + +
    ) } @@ -104,7 +108,13 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.milestoneTemplates)) && + !props.isRemoving +) const MilestoneTemplateDetailsWithLoaderEnhanced = enhance(errorHandler(MilestoneTemplateDetails)) const MilestoneTemplateDetailsWithLoaderAndAuth = requiresAuthentication(MilestoneTemplateDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/PlanConfigDetails.jsx b/src/routes/metadata/containers/PlanConfigDetails.jsx index 1c84a796d..3f2bd781b 100644 --- a/src/routes/metadata/containers/PlanConfigDetails.jsx +++ b/src/routes/metadata/containers/PlanConfigDetails.jsx @@ -16,6 +16,7 @@ import { getRevisionList, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -68,6 +69,7 @@ class PlanConfigDetails extends React.Component { templates, isAdmin, match, + isLoading } = this.props const key = match.params.key let planConfig @@ -76,20 +78,23 @@ class PlanConfigDetails extends React.Component { } return (
    - + {isLoading && ()} +
    + +
    ) } @@ -138,7 +143,14 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving && !props.versionOptionsLoading) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.versionMetadata)) && + !props.isRemoving && + !props.versionOptionsLoading +) const PlanConfigDetailsWithLoaderEnhanced = enhance(errorHandler(PlanConfigDetails)) const PlanConfigDetailsWithLoaderAndAuth = requiresAuthentication(PlanConfigDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/PriceConfigDetails.jsx b/src/routes/metadata/containers/PriceConfigDetails.jsx index 9a098e979..b6954f965 100644 --- a/src/routes/metadata/containers/PriceConfigDetails.jsx +++ b/src/routes/metadata/containers/PriceConfigDetails.jsx @@ -16,6 +16,7 @@ import { getRevisionList, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -68,6 +69,7 @@ class PriceConfigDetails extends React.Component { templates, isAdmin, match, + isLoading } = this.props const key = match.params.key let priceConfig @@ -76,20 +78,23 @@ class PriceConfigDetails extends React.Component { } return (
    - + {isLoading && ()} +
    + +
    ) } @@ -138,7 +143,14 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving && !props.versionOptionsLoading) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.versionMetadata)) && + !props.isRemoving && + !props.versionOptionsLoading +) const PriceConfigDetailsWithLoaderEnhanced = enhance(errorHandler(PriceConfigDetails)) const PriceConfigDetailsWithLoaderAndAuth = requiresAuthentication(PriceConfigDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/ProductCategoryDetails.jsx b/src/routes/metadata/containers/ProductCategoryDetails.jsx index f94a177a5..7fd6d4285 100644 --- a/src/routes/metadata/containers/ProductCategoryDetails.jsx +++ b/src/routes/metadata/containers/ProductCategoryDetails.jsx @@ -13,6 +13,7 @@ import { updateProjectsMetadata, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -43,7 +44,7 @@ class ProductCategoryDetails extends React.Component { createProductCategory, updateProjectsMetadata, templates, - // isLoading, + isLoading, isAdmin, match, } = this.props @@ -52,17 +53,20 @@ class ProductCategoryDetails extends React.Component { const productCategory = _.find(productCategories, t => t.key === key) return (
    - + {isLoading && ()} +
    + +
    ) } @@ -104,7 +108,13 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.productCategories)) && + !props.isRemoving +) const ProductCategoryDetailsWithLoaderEnhanced = enhance(errorHandler(ProductCategoryDetails)) const ProductCategoryDetailsWithLoaderAndAuth = requiresAuthentication(ProductCategoryDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/ProductTemplateDetails.jsx b/src/routes/metadata/containers/ProductTemplateDetails.jsx index 8bb8cbf91..fe72b0cf4 100644 --- a/src/routes/metadata/containers/ProductTemplateDetails.jsx +++ b/src/routes/metadata/containers/ProductTemplateDetails.jsx @@ -13,6 +13,7 @@ import { createProductTemplate, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -43,7 +44,7 @@ class ProductTemplateDetails extends React.Component { createProductTemplate, updateProjectsMetadata, templates, - // isLoading, + isLoading, isAdmin, match, } = this.props @@ -53,17 +54,20 @@ class ProductTemplateDetails extends React.Component { const productTemplate = _.find(productTemplates, t => t.id === templateId) return (
    - + {isLoading && ()} +
    + +
    ) } @@ -106,7 +110,13 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.errorTemp) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.productTemplates)) && + !props.isRemoving +) const ProductTemplateDetailsWithLoaderEnhanced = enhance(errorHandler(ProductTemplateDetails)) const ProductTemplateDetailsWithLoaderAndAuth = requiresAuthentication(ProductTemplateDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/ProjectTemplateDetails.jsx b/src/routes/metadata/containers/ProjectTemplateDetails.jsx index 536f2a2a3..cacc4820c 100644 --- a/src/routes/metadata/containers/ProjectTemplateDetails.jsx +++ b/src/routes/metadata/containers/ProjectTemplateDetails.jsx @@ -14,6 +14,7 @@ import { } from '../../../actions/templates' import { fireProjectDirty } from '../../../projects/actions/project' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -44,7 +45,7 @@ class ProjectTemplateDetails extends React.Component { createProjectTemplate, updateProjectsMetadata, templates, - // isLoading, + isLoading, isAdmin, match, previewProject, @@ -56,19 +57,22 @@ class ProjectTemplateDetails extends React.Component { const projectTemplate = _.find(projectTemplates, t => t.id === templateId) return (
    - + {isLoading && ()} +
    + +
    ) } @@ -115,7 +119,13 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.errorTemp) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.projectTemplates)) && + !props.isRemoving +) const ProjectTemplateDetailsWithLoaderEnhanced = enhance(errorHandler(ProjectTemplateDetails)) const ProjectTemplateDetailsWithLoaderAndAuth = requiresAuthentication(ProjectTemplateDetailsWithLoaderEnhanced) diff --git a/src/routes/metadata/containers/ProjectTypeDetails.jsx b/src/routes/metadata/containers/ProjectTypeDetails.jsx index c261146e1..3a5c7bf1f 100644 --- a/src/routes/metadata/containers/ProjectTypeDetails.jsx +++ b/src/routes/metadata/containers/ProjectTypeDetails.jsx @@ -13,6 +13,7 @@ import { updateProjectsMetadata, } from '../../../actions/templates' import spinnerWhileLoading from '../../../components/LoadingSpinner' +import LoadingIndicator from '../../../components/LoadingIndicator/LoadingIndicator' import CoderBot from '../../../components/CoderBot/CoderBot' import { requiresAuthentication } from '../../../components/AuthenticatedComponent' import MetaDataPanel from '../components/MetaDataPanel' @@ -43,7 +44,7 @@ class ProjectTypeDetails extends React.Component { createProjectType, updateProjectsMetadata, templates, - // isLoading, + isLoading, isAdmin, match, } = this.props @@ -52,17 +53,20 @@ class ProjectTypeDetails extends React.Component { const projectType = _.find(projectTypes, t => t.key === key) return (
    - + {isLoading && ()} +
    + +
    ) } @@ -104,7 +108,13 @@ const page500 = compose( const showErrorMessageIfError = hasLoaded => branch(hasLoaded, renderComponent(page500(CoderBot)), t => t) const errorHandler = showErrorMessageIfError(props => props.error) -const enhance = spinnerWhileLoading(props => !props.isLoading && !props.isRemoving) +const enhance = spinnerWhileLoading( + props => + (!props.isLoading || + // avoid resetting state of child when saving + (props.templates && props.templates.projectTypes)) && + !props.isRemoving +) const ProjectTypeDetailsWithLoaderEnhanced = enhance(errorHandler(ProjectTypeDetails)) const ProjectTypeDetailsWithLoaderAndAuth = requiresAuthentication(ProjectTypeDetailsWithLoaderEnhanced) diff --git a/src/routes/notifications/helpers/notifications.js b/src/routes/notifications/helpers/notifications.js index 16540ec58..36fcbe193 100644 --- a/src/routes/notifications/helpers/notifications.js +++ b/src/routes/notifications/helpers/notifications.js @@ -262,6 +262,20 @@ export const filterTopicAndPostChangedNotifications = (notifications, tagRegExp) return topicAndPostNotifications } +/** + * Filter notifications about Link and File changed + * + * @param {Array} notifications list of notifications + * + * @return {Array} notifications list filtered of notifications + */ +export const filterFileAndLinkChangedNotifications = (notifications) => { + return _.filter(notifications, (notification) => ( + notification.eventType === EVENT_TYPE.PROJECT.FILE_UPLOADED || + notification.eventType === EVENT_TYPE.PROJECT.LINK_CREATED + )) +} + /** * Filter notification about post mentions * @param {Array} notifications list of notifications