diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f70ab047de..974ef7fc6b 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -17,7 +17,9 @@ "add_email_to_claim_features": "", "add_files": "", "add_new_email": "", + "add_or_remove_project_from_tag": "", "add_role_and_department": "", + "add_to_folder": "", "all_projects": "", "also": "", "anyone_with_link_can_edit": "", diff --git a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx index c915b561cb..3eb379b5ff 100644 --- a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx +++ b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx @@ -1,4 +1,4 @@ -import _ from 'lodash' +import { sortBy } from 'lodash' import { Button } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import ColorManager from '../../../../ide/colors/ColorManager' @@ -34,7 +34,7 @@ export default function TagsList() { {t('new_folder')} - {_.sortBy(tags, ['name']).map((tag, index) => { + {sortBy(tags, tag => tag.name.toLowerCase()).map((tag, index) => { return (
  • { + e.preventDefault() + openCreateTagModal() + }, + [openCreateTagModal] + ) + + const handleAddTagToSelectedProjects = useCallback( + (e, tagId) => { + e.preventDefault() + const tag = tags.find(tag => tag._id === tagId) + for (const selectedProject of selectedProjects) { + if (!tag?.project_ids?.includes(selectedProject.id)) { + addProjectToTagInView(tagId, selectedProject.id) + addProjectToTag(tagId, selectedProject.id) + } + } + }, + [tags, selectedProjects, addProjectToTagInView] + ) + + const handleRemoveTagFromSelectedProjects = useCallback( + (e, tagId) => { + e.preventDefault() + for (const selectedProject of selectedProjects) { + removeProjectFromTagInView(tagId, selectedProject.id) + removeProjectFromTag(tagId, selectedProject.id) + } + }, + [selectedProjects, removeProjectFromTagInView] + ) + + const containsAllSelectedProjects = useCallback( + tag => { + for (const project of selectedProjects) { + if (!(tag.project_ids || []).includes(project.id)) { + return false + } + } + return true + }, + [selectedProjects] + ) + + const containsSomeSelectedProjects = useCallback( + tag => { + for (const project of selectedProjects) { + if (tag.project_ids?.includes(project.id)) { + return true + } + } + return false + }, + [selectedProjects] + ) + + return ( + <> + + + + + +
  • + {t('add_to_folder')} +
  • + {sortBy(tags, tag => tag.name.toLowerCase()).map(tag => { + return ( +
  • + +
  • + ) + })} +
  • +
  • + +
  • + + + + + ) +} + +export default memo(TagsDropdown) diff --git a/services/web/frontend/js/features/project-list/components/table/project-tools/project-tools.tsx b/services/web/frontend/js/features/project-list/components/table/project-tools/project-tools.tsx index 7798abd052..68dee26ac8 100644 --- a/services/web/frontend/js/features/project-list/components/table/project-tools/project-tools.tsx +++ b/services/web/frontend/js/features/project-list/components/table/project-tools/project-tools.tsx @@ -4,6 +4,7 @@ import { useProjectListContext } from '../../../context/project-list-context' import ArchiveProjectsButton from './buttons/archive-projects-button' import DownloadProjectsButton from './buttons/download-projects-button' import ProjectToolsMoreDropdownButton from './buttons/project-tools-more-dropdown-button' +import TagsDropdown from './buttons/tags-dropdown' import TrashProjectsButton from './buttons/trash-projects-button' import UnarchiveProjectsButton from './buttons/unarchive-projects-button' import UntrashProjectsButton from './buttons/untrash-projects-button' @@ -23,6 +24,8 @@ function ProjectTools() { {filter === 'archived' && } + {!['archived', 'trashed'].includes(filter) && } + {selectedProjects.length === 1 && filter !== 'archived' && filter !== 'trashed' && } diff --git a/services/web/frontend/js/features/project-list/context/project-list-context.tsx b/services/web/frontend/js/features/project-list/context/project-list-context.tsx index bfc5b51c88..9ca6264962 100644 --- a/services/web/frontend/js/features/project-list/context/project-list-context.tsx +++ b/services/web/frontend/js/features/project-list/context/project-list-context.tsx @@ -83,6 +83,7 @@ type ProjectListContextValue = { deleteTag: (tagId: string) => void updateProjectViewData: (newProjectData: Project) => void removeProjectFromView: (project: Project) => void + addProjectToTagInView: (tagId: string, projectId: string) => void removeProjectFromTagInView: (tagId: string, projectId: string) => void searchText: string setSearchText: React.Dispatch> @@ -302,6 +303,21 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) { [setTags] ) + const addProjectToTagInView = useCallback( + (tagId: string, projectId: string) => { + setTags(tags => { + const updatedTags = [...tags] + for (const tag of updatedTags) { + if (tag._id === tagId) { + tag.project_ids = uniq([...(tag.project_ids || []), projectId]) + } + } + return updatedTags + }) + }, + [setTags] + ) + const removeProjectFromTagInView = useCallback( (tagId: string, projectId: string) => { setTags(tags => { @@ -368,7 +384,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) { () => ({ addTag, addClonedProjectToViewData, - selectOrUnselectAllProjects, + addProjectToTagInView, deleteTag, error, filter, @@ -383,6 +399,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) { selectedTagId, selectFilter, selectedProjects, + selectOrUnselectAllProjects, selectTag, searchText, setSearchText, @@ -398,7 +415,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) { [ addTag, addClonedProjectToViewData, - selectOrUnselectAllProjects, + addProjectToTagInView, deleteTag, error, filter, @@ -413,6 +430,7 @@ export function ProjectListProvider({ children }: ProjectListProviderProps) { selectedTagId, selectFilter, selectedProjects, + selectOrUnselectAllProjects, selectTag, searchText, setSearchText, diff --git a/services/web/frontend/js/features/project-list/hooks/use-tag.tsx b/services/web/frontend/js/features/project-list/hooks/use-tag.tsx index 15546e18b8..adc4189f67 100644 --- a/services/web/frontend/js/features/project-list/hooks/use-tag.tsx +++ b/services/web/frontend/js/features/project-list/hooks/use-tag.tsx @@ -6,10 +6,18 @@ import RenameTagModal from '../components/modals/rename-tag-modal' import DeleteTagModal from '../components/modals/delete-tag-modal' import EditTagModal from '../components/modals/edit-tag-modal' import { find } from 'lodash' +import { addProjectToTag } from '../util/api' function useTag() { - const { tags, selectTag, addTag, renameTag, deleteTag } = - useProjectListContext() + const { + tags, + selectTag, + addTag, + renameTag, + deleteTag, + selectedProjects, + addProjectToTagInView, + } = useProjectListContext() const [creatingTag, setCreatingTag] = useState(false) const [renamingTag, setRenamingTag] = useState() const [deletingTag, setDeletingTag] = useState() @@ -31,8 +39,12 @@ function useTag() { (tag: Tag) => { setCreatingTag(false) addTag(tag) + for (const selectedProject of selectedProjects) { + addProjectToTagInView(tag._id, selectedProject.id) + addProjectToTag(tag._id, selectedProject.id) + } }, - [addTag] + [addTag, selectedProjects, addProjectToTagInView] ) const handleRenameTag = useCallback( diff --git a/services/web/frontend/js/features/project-list/util/api.ts b/services/web/frontend/js/features/project-list/util/api.ts index 129664ec0d..1405b7ccdb 100644 --- a/services/web/frontend/js/features/project-list/util/api.ts +++ b/services/web/frontend/js/features/project-list/util/api.ts @@ -25,6 +25,10 @@ export function deleteTag(tagId: string) { return deleteJSON(`/tag/${tagId}`) } +export function addProjectToTag(tagId: string, projectId: string) { + return postJSON(`/tag/${tagId}/project/${projectId}`) +} + export function removeProjectFromTag(tagId: string, projectId: string) { return deleteJSON(`/tag/${tagId}/project/${projectId}`) } diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less index b203e0a5bb..6af532186e 100644 --- a/services/web/frontend/stylesheets/app/project-list-react.less +++ b/services/web/frontend/stylesheets/app/project-list-react.less @@ -643,3 +643,30 @@ .project-list-load-more-button { margin-bottom: @margin-sm; } + +.tag-dropdown-button { + display: block; + width: 100%; + font-weight: normal; + font-size: @font-size-base; + text-align: left; + color: @gray-dark; + background-color: transparent; + border-radius: unset; + border: none; + border-bottom: solid 1px transparent; + padding: 3px 20px; + + &:active, + &:focus { + color: @gray-dark; + background-color: transparent; + text-decoration: none; + outline: none; + } + + &:hover { + color: @white; + background-color: @ol-green; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index b27f5e5ea7..87f074a82c 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1887,6 +1887,7 @@ "you_dont_have_any_repositories": "You don’t have any repositories", "tag_name_cannot_exceed_characters": "Tag name cannot exceed __maxLength__ characters", "tag_name_is_already_used": "Tag \"__tagName__\" already exists", + "add_or_remove_project_from_tag": "Add or remove project from tag __tagName__", "save_changes": "Save changes", "labs_program_already_participating": "You are enrolled in Labs", "labs_program_not_participating": "You are not enrolled in Labs", diff --git a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx index b2ce94fff3..f321f094c6 100644 --- a/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx +++ b/services/web/test/frontend/features/project-list/components/table/project-tools/project-tools.test.tsx @@ -13,9 +13,11 @@ describe('', function () { it('renders the project tools for the all projects filter', function () { renderWithProjectListContext() - expect(screen.getAllByRole('button').length).to.equal(3) + expect(screen.getAllByRole('button').length).to.equal(5) screen.getByLabelText('Download') screen.getByLabelText('Archive') screen.getByLabelText('Trash') + screen.getByTitle('Tags') + screen.getByRole('button', { name: 'Create New Folder' }) }) })