From 966565b967d923792169cfc708382ac47b8194d3 Mon Sep 17 00:00:00 2001 From: Vigneshkumar Chinnachamy M Date: Mon, 16 Mar 2020 14:29:03 +0530 Subject: [PATCH 1/2] challenge-30119190-links-as-attachments --- src/api/projectAttachments.js | 21 ++- src/components/AssetsLibrary/AddLink.jsx | 9 ++ .../AssetsLibrary/FilesGridView.jsx | 26 +++- .../AssetsLibrary/FilterColHeader.jsx | 78 ++++++++-- src/components/AssetsLibrary/GridView.scss | 30 ++++ .../AssetsLibrary/LinksGridView.jsx | 50 +++--- src/components/AssetsLibrary/SubFolder.jsx | 24 +-- .../FileList/AddFilePermissions.jsx | 27 +++- .../FileList/AddFilePermissions.scss | 9 ++ src/components/FileList/FileList.scss | 35 ++++- src/components/FileList/FileListItem.jsx | 58 +++++-- .../LinksMenu/EditFileAttachment.jsx | 24 ++- src/components/LinksMenu/EditLinkModal.jsx | 33 +++- src/components/Select/FormsySelect.jsx | 3 +- src/components/TagSelect/TagSelect.jsx | 54 +++++++ src/projects/actions/projectAttachment.js | 4 +- .../detail/components/FileListContainer.jsx | 11 +- .../detail/containers/AssetsInfoContainer.jsx | 147 +++++++++++------- src/projects/reducers/project.js | 7 +- 19 files changed, 492 insertions(+), 158 deletions(-) create mode 100644 src/components/TagSelect/TagSelect.jsx diff --git a/src/api/projectAttachments.js b/src/api/projectAttachments.js index e9f89b887..037ee3f0f 100644 --- a/src/api/projectAttachments.js +++ b/src/api/projectAttachments.js @@ -2,8 +2,17 @@ import { axiosInstance as axios } from './requestInterceptor' import { PROJECTS_API_URL, FILE_PICKER_SUBMISSION_CONTAINER_NAME } from '../config/constants' export function addProjectAttachment(projectId, fileData) { - // add s3 bucket prop - fileData.s3Bucket = FILE_PICKER_SUBMISSION_CONTAINER_NAME + + if (fileData.type === 'file') { + // add s3 bucket prop + fileData.s3Bucket = FILE_PICKER_SUBMISSION_CONTAINER_NAME + } + + // The api takes only arrays + if (!fileData.tags) { + fileData.tags = [] + } + return axios.post(`${PROJECTS_API_URL}/v5/projects/${projectId}/attachments`, fileData) .then( resp => { resp.data.downloadUrl = `/projects/${projectId}/attachments/${resp.data.id}` @@ -19,6 +28,14 @@ export function updateProjectAttachment(projectId, attachmentId, attachment) { } } + // The api takes only arrays + if (attachment && !attachment.tags) { + attachment = { + ...attachment, + tags: [] + } + } + return axios.patch( `${PROJECTS_API_URL}/v5/projects/${projectId}/attachments/${attachmentId}`, attachment) .then ( resp => { diff --git a/src/components/AssetsLibrary/AddLink.jsx b/src/components/AssetsLibrary/AddLink.jsx index 053b619d1..8538a8605 100644 --- a/src/components/AssetsLibrary/AddLink.jsx +++ b/src/components/AssetsLibrary/AddLink.jsx @@ -7,6 +7,7 @@ const TCFormFields = FormsyForm.Fields const Formsy = FormsyForm.Formsy import './AddLink.scss' +import { TagSelect } from '../TagSelect/TagSelect' class AddLink extends React.Component { constructor(props) { @@ -75,6 +76,14 @@ class AddLink extends React.Component { }} wrapperClass="form-group" /> +
+ + +
+
@@ -241,8 +245,8 @@ const FilesGridView = ({ const onDeleteCancel = () => onDeleteIntent(-1) const handleDeleteClick = () => onDeleteIntent(idx) - const onEditConfirm = (title, allowedUsers) => { - onEdit(link.id, title, allowedUsers) + const onEditConfirm = (title, allowedUsers, tags) => { + onEdit(link, title, allowedUsers, tags) onEditIntent(-1) } const onEditCancel = () => onEditIntent(-1) @@ -303,7 +307,14 @@ const FilesGridView = ({
-

{renderLink(link)}

+
+
+

{renderLink(link)}

+ { + link.tags && link.tags.map((t, i) => {t}) + } +
+
{renderSharedWith(link)}
@@ -338,6 +349,7 @@ FilesGridView.propTypes = { canEdit: PropTypes.bool, links: PropTypes.array.isRequired, selectedUsers: PropTypes.string, + selectedTags: PropTypes.arrayOf(PropTypes.string), projectMembers: PropTypes.object, pendingAttachments: PropTypes.object, onUploadAttachment: PropTypes.func, diff --git a/src/components/AssetsLibrary/FilterColHeader.jsx b/src/components/AssetsLibrary/FilterColHeader.jsx index 8bd34ce2e..f4d752fb7 100644 --- a/src/components/AssetsLibrary/FilterColHeader.jsx +++ b/src/components/AssetsLibrary/FilterColHeader.jsx @@ -9,60 +9,82 @@ const Formsy = FormsyForm.Formsy const TCFormFields = FormsyForm.Fields class FilterColHeader extends React.Component { - + constructor(props) { super(props) - this.state = {value: '', from: '', to: ''} + this.state = {value: '', from: '', to: '', name: '', tag: ''} this.setFilter = this.setFilter.bind(this) this.onChange = this.onChange.bind(this) this.onFromDateChange = this.onFromDateChange.bind(this) this.onToDateChange = this.onToDateChange.bind(this) + this.onNameChange = this.onNameChange.bind(this) + this.onTagChange = this.onTagChange.bind(this) } - + setFilter(newfilter) { this.props.setFilter(this.props.filterName, newfilter) } - + onChange(event) { this.setState({value: event.target.value}) - + setTimeout(() => { this.setFilter(this.state.value) }) } - + onFromDateChange(name, value) { this.setState({from: value}) - + setTimeout(() => { this.props.setFilter('date.from', this.state.from) }) } - + onToDateChange(name, value) { this.setState({to: value}) - + setTimeout(() => { this.props.setFilter('date.to', this.state.to) }) } - + + onNameChange(name, value) { + this.setState({ name: value}) + + setTimeout(() => { + this.props.setFilter('name.name', this.state.name) + }) + } + + onTagChange(name, value) { + this.setState({tag: value}) + + setTimeout(() => { + this.props.setFilter('name.tag', this.state.tag) + }) + } + componentDidMount() { this.setState({ value: this.props.value || '', from: this.props.from || '', - to: this.props.to || '' + to: this.props.to || '', + name: this.props.name || '', + tag: this.props.tag || '' }) } - + clearFilter() { this.setState({ value: '', from: '', - to: '' + to: '', + name: '', + tag: '' }) } - + renderByType() { switch (this.props.type) { case 'date': { @@ -85,7 +107,29 @@ class FilterColHeader extends React.Component { ) } - + + case 'name': { + return ( + + + + + + ) + } + default: { return ( diff --git a/src/components/AssetsLibrary/GridView.scss b/src/components/AssetsLibrary/GridView.scss index eda14f995..d8595372a 100644 --- a/src/components/AssetsLibrary/GridView.scss +++ b/src/components/AssetsLibrary/GridView.scss @@ -65,11 +65,14 @@ -ms-flex: none; flex: none; width: 60px; + line-height: 30px; + align-items: flex-start; svg, img { height: 30px; width: 30px; margin-left: 4px; + margin-top: 8px; } } @@ -82,6 +85,8 @@ line-height: 30px; min-width: 0; text-align: left; + align-items: flex-start; + padding-right: 4px; > p { overflow: hidden; @@ -90,10 +95,16 @@ } } +.item-name-tag-wrapper { + overflow: hidden; +} + .item-shared-with { flex: none; width: 140px; word-break: keep-all; + align-items: flex-start; + line-height: 30px; } .item-modified, .item-created-by { @@ -114,6 +125,7 @@ width: 50px; justify-content: flex-end; padding-right: 4px; + align-items: flex-start; } .item-action .link-buttons { @@ -145,6 +157,24 @@ position: relative; } +.tag { + @include roboto-medium; + background-color: $tc-gray-10; + border-radius: 4px; + color: $tc-gray-50; + font-size: 13px; + height: 20px; + line-height: 20px; + padding: 0 2 * $base-unit; + margin-top: $base-unit; + margin-right: $base-unit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + display: inline-block; +} + :global { .user-block .tooltip-container { text-align: left; diff --git a/src/components/AssetsLibrary/LinksGridView.jsx b/src/components/AssetsLibrary/LinksGridView.jsx index 82b21d660..48b449315 100644 --- a/src/components/AssetsLibrary/LinksGridView.jsx +++ b/src/components/AssetsLibrary/LinksGridView.jsx @@ -49,7 +49,7 @@ const LinksGridView = ({ let nameFieldRef let sharedWithFieldRef let dateFieldRef - + const updateSubContents = () => { if (selectedLink) { let link = links.filter(item => { @@ -57,29 +57,29 @@ const LinksGridView = ({ && selectedLink.createdBy === item.createdBy && selectedLink.updatedAt === item.updatedAt })[0] - + if (!link) { link = _.cloneDeep(selectedLink) link.children = [] } - + onChangeSubFolder(link) } } - + const clearSubContents = () => clearing = true - + const clearFieldValues = () => { nameFieldRef.clearFilter() sharedWithFieldRef.clearFilter() dateFieldRef.clearFilter() } - + const renderLink = (link) => { if (link.onClick) { return ( { // we only prevent default on click, // as we handle clicks with
  • @@ -91,28 +91,28 @@ const LinksGridView = ({ {link.title} ) } else if (link.noNewPage) { - return {link.title} + return {link.title} } else { - return {link.title} + return {link.title} } } const goBack = () => { onChangeSubFolder(null) selectedLink = null } - + const getSharedWithText = (tag) => { return tag === PROJECT_FEED_TYPE_MESSAGES ? PROJECT_ASSETS_SHARED_WITH_TOPCODER_MEMBERS : PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS } - + if (clearing) { setTimeout(() => { updateSubContents() clearing = false }) } - + return (
    {(subFolderContent) && ( @@ -155,8 +155,9 @@ const LinksGridView = ({ ref={(comp) => nameFieldRef = comp} title="Name" setFilter={setFilter} - filterName="name" - value={getFilterValue('name')} + type="name" + name={getFilterValue('name.name')} + tag={getFilterValue('name.tag')} />
    @@ -183,14 +184,14 @@ const LinksGridView = ({
  • {links.map((link, idx) => { const onDeleteConfirm = () => { - onDelete(idx) + onDelete(link.id) onDeleteIntent(-1) } const onDeleteCancel = () => onDeleteIntent(-1) const handleDeleteClick = () => onDeleteIntent(idx) - const onEditConfirm = (title, address) => { - onEdit(idx, title, address) + const onEditConfirm = (title, address, tags) => { + onEdit(link.id, title, address, tags) onEditIntent(-1) } const onEditCancel = () => onEditIntent(-1) @@ -200,12 +201,14 @@ const LinksGridView = ({ selectedLink = link } const owner = _.find(assetsMembers, m => m.userId === _.parseInt(link.createdBy)) - + if (Array.isArray(link.children) && link.children.length > 0) { return (
  • -

    {formatFolderTitle(link.title)}

    +
    +

    {formatFolderTitle(link.title)}

    +

    {getSharedWithText(link.tag)} @@ -247,7 +250,14 @@ const LinksGridView = ({ return (

  • -

    {renderLink(link)}

    +
    +
    +

    {renderLink(link)}

    + { + link.tags && link.tags.map((t, i) => {t}) + } +
    +

    {getSharedWithText(link.tag)} diff --git a/src/components/AssetsLibrary/SubFolder.jsx b/src/components/AssetsLibrary/SubFolder.jsx index 315f9d8a2..811cd0c35 100644 --- a/src/components/AssetsLibrary/SubFolder.jsx +++ b/src/components/AssetsLibrary/SubFolder.jsx @@ -29,12 +29,12 @@ class SubFolder extends React.Component { this.clearFieldValues = this.clearFieldValues.bind(this) this.wrappedSetFilter = this.wrappedSetFilter.bind(this) } - + componentDidMount() { // scroll to the top when open document.body.scrollTop = document.documentElement.scrollTop = 0 } - + onDeleteConfirm() { const link = this.props.link.children[this.state.linkToDelete] if (link) { @@ -42,39 +42,39 @@ class SubFolder extends React.Component { this.onDeleteCancel() } } - + onDeleteCancel() { this.setState({ linkToDelete: -1 }) } - + deleteLink(idx) { this.setState({ linkToDelete: idx }) } - + hasAccess(createdBy) { const { loggedInUser } = this.props return Number.parseInt(createdBy) === loggedInUser.userId } - + isURLValid(linkAddress) { return /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/.test(linkAddress) } - + clearFieldValues() { this.nameFieldRef.clearFilter() this.sharedWithFieldRef.clearFilter() this.dateFieldRef.clearFilter() } - + wrappedSetFilter(name, value) { this.props.setFilter(name, value) this.props.updateSubContents() } - + render() { const { link, @@ -89,7 +89,7 @@ class SubFolder extends React.Component { filtered } = this.props const { linkToDelete } = this.state - + return (

    = 0)}, '')}> {(linkToDelete >= 0) &&
    } @@ -115,9 +115,9 @@ class SubFolder extends React.Component { this.nameFieldRef = comp} title="Name" - filterName="name" + filterName="name.name" setFilter={this.wrappedSetFilter} - value={getFilterValue('name')} + value={getFilterValue('name.name')} />
    diff --git a/src/components/FileList/AddFilePermissions.jsx b/src/components/FileList/AddFilePermissions.jsx index 0a44b3433..008f9d863 100644 --- a/src/components/FileList/AddFilePermissions.jsx +++ b/src/components/FileList/AddFilePermissions.jsx @@ -8,8 +8,9 @@ import LoadingIndicator from '../LoadingIndicator/LoadingIndicator' import './AddFilePermissions.scss' import XMarkIcon from '../../assets/icons/icon-x-mark.svg' +import { TagSelect } from '../TagSelect/TagSelect' -const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projectMembers, loggedInUser, isSharingAttachment }) => { +const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, selectedTags, projectMembers, loggedInUser, isSharingAttachment }) => { selectedUsers = selectedUsers || '' const mapHandlesToUserIds = handles => { const projectMembersByHandle = mapKeys(projectMembers, value => value.handle) @@ -25,7 +26,7 @@ const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projec >
    - Who do you want to share this file with? + Attachment Options
    @@ -33,12 +34,26 @@ const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projec { isSharingAttachment && }
    - {/* Share with all members */}
    + {/* Tags */} +
    + Tags +
    + onChange(selectedUsers, tags)} + /> + + {/* Permissions */} +
    + Who do you want to share this file with? +
    + + {/* Share with all members */}
    @@ -51,13 +66,13 @@ const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projec ({ value: handle, label: handle })) : []} - onUpdate={onChange} + onUpdate={users => onChange(users, selectedTags)} loggedInUser={loggedInUser} />
    diff --git a/src/components/FileList/AddFilePermissions.scss b/src/components/FileList/AddFilePermissions.scss index 79070ba68..4ae41cbb1 100644 --- a/src/components/FileList/AddFilePermissions.scss +++ b/src/components/FileList/AddFilePermissions.scss @@ -1,3 +1,5 @@ +@import '~tc-ui/src/styles/tc-includes'; + .btn-all-members { text-align: center; margin-top: 8px; @@ -10,4 +12,11 @@ .btn-selected-members { margin-top: 10px; +} + +.dialog-sub-title { + @include roboto; + + margin-top: 30px; + margin-bottom: 10px; } \ No newline at end of file diff --git a/src/components/FileList/FileList.scss b/src/components/FileList/FileList.scss index 93a243805..bf0ac9b43 100644 --- a/src/components/FileList/FileList.scss +++ b/src/components/FileList/FileList.scss @@ -120,15 +120,16 @@ } .title-edit { - display: flex; - align-items: center; margin-bottom: 10px; + .title-with-action-btns { + display: flex; + } + input { @include roboto-bold; font-size: 15px; color: $tc-gray-80; - width: 80%; height: 30px; margin-left: 0; margin-bottom: 0; @@ -136,6 +137,28 @@ } } + .restrict-access-edit { + margin-bottom: 10px; + } + + .tag { + @include roboto-medium; + background-color: $tc-gray-10; + border-radius: 4px; + color: $tc-gray-50; + font-size: 13px; + height: 20px; + line-height: 20px; + padding: 0 2 * $base-unit; + margin-top: $base-unit; + margin-right: $base-unit; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + display: inline-block; + } + .tc-textarea { margin-left: 0; padding: 5px 10px; @@ -159,6 +182,10 @@ margin-left: 22px; } + .save-icons { + white-space: nowrap; + } + .icon-save { margin-right: 25px; @@ -168,7 +195,7 @@ } .icon-close { - margin-right: 10px; + margin-right: 2px; @media screen and (max-width: $screen-md - 1px) { margin-right: 0; diff --git a/src/components/FileList/FileListItem.jsx b/src/components/FileList/FileListItem.jsx index d33f148a5..2ad0a4006 100644 --- a/src/components/FileList/FileListItem.jsx +++ b/src/components/FileList/FileListItem.jsx @@ -11,7 +11,7 @@ import CloseIcon from '../../assets/icons/icon-close.svg' import EditIcon from '../../assets/icons/icon-edit.svg' import SaveIcon from '../../assets/icons/icon-save.svg' import UserAutoComplete from '../UserAutoComplete/UserAutoComplete' - +import { TagSelect } from '../TagSelect/TagSelect' export default class FileListItem extends React.Component { @@ -21,6 +21,7 @@ export default class FileListItem extends React.Component { title: props.title, description: props.description, allowedUsers: props.allowedUsers, + tags: props.tags, isEditing: false } this.handleSave = this.handleSave.bind(this) @@ -52,7 +53,7 @@ export default class FileListItem extends React.Component { if (!_.isEmpty(errors)) { this.setState({ errors }) } else { - this.props.onSave(this.props.id, {title, description: this.refs.desc.value, allowedUsers: this.state.allowedUsers}, e) + this.props.onSave(this.props.id, {title, description: this.refs.desc.value, allowedUsers: this.state.allowedUsers, tags: this.state.tags}, e) this.setState({isEditing: false}) } } @@ -84,6 +85,10 @@ export default class FileListItem extends React.Component { }) } + onTagsChange(tags) { + this.setState({ tags }) + } + userIdsToHandles(allowedUsers) { const { projectMembers } = this.props allowedUsers = allowedUsers || [] @@ -99,34 +104,54 @@ export default class FileListItem extends React.Component { renderEditing() { const { title, description, projectMembers, loggedInUser, askForPermissions } = this.props - const { errors, allowedUsers } = this.state + const { errors, allowedUsers, tags } = this.state const onExitEdit = () => this.setState({isEditing: false, errors: {} }) return (
    + {/* Title */}
    - -
    - - +
    + +
    + + +
    + +
    { (errors && errors.title) &&
    { errors.title }
    } + + {/* Description */} +