From 49d0a4990d5529b453c6fe958356468caa67a921 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Fri, 24 Apr 2026 08:08:27 +0530 Subject: [PATCH] OpenConceptLab/ocl_issues#2485 | Copy project --- src/components/map-projects/Controls.jsx | 13 +++++- src/components/map-projects/MapProject.jsx | 46 ++++++++++++++++++++- src/components/map-projects/MapProjects.jsx | 25 +++++++++-- src/i18n/locales/en/translations.json | 4 +- src/i18n/locales/es/translations.json | 4 +- src/i18n/locales/zh/translations.json | 4 +- 6 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/components/map-projects/Controls.jsx b/src/components/map-projects/Controls.jsx index d027693..0e324e0 100644 --- a/src/components/map-projects/Controls.jsx +++ b/src/components/map-projects/Controls.jsx @@ -15,6 +15,7 @@ import TimelineIcon from '@mui/icons-material/Timeline'; import SettingsIcon from '@mui/icons-material/Settings'; import SaveIcon from '@mui/icons-material/Save'; import DeleteIcon from '@mui/icons-material/Delete'; +import CopyIcon from '@mui/icons-material/CopyAll'; import JSONIcon from '@mui/icons-material/DataObject'; import { copyToClipboard } from '../../common/utils' import RepoIcon from '../repos/RepoIcon' @@ -38,7 +39,7 @@ const IkonButton = ({title, icon, onClick, color, disabled, id}) => { ) } -const Controls = ({project, onDownload, onSave, onDelete, owner, file, isSaving, onImport, importResponse, onDownloadImportReport, onProjectLogsClick, isProjectsLogOpen, configure, setConfigure, isCoreUser, loadingMatches}) => { +const Controls = ({project, onDownload, onSave, onDelete, owner, file, isSaving, onImport, importResponse, onDownloadImportReport, onProjectLogsClick, isProjectsLogOpen, configure, setConfigure, isCoreUser, loadingMatches, onCopyClick}) => { const { t } = useTranslation(); const [anchorEl, setAnchorEl] = React.useState(null); const downloadOpen = Boolean(anchorEl); @@ -76,6 +77,16 @@ const Controls = ({project, onDownload, onSave, onDelete, owner, file, isSaving, icon={} disabled={loadingMatches} /> + { + project?.id && + } + /> + } { project?.id && { const user = getCurrentUser() const params = useParams() const history = useHistory() + const location = useLocation() + const copyFromProjectURL = React.useMemo(() => { + const queryParams = new URLSearchParams(location.search) + return queryParams.get('copyFrom') + }, [location.search]) + const bridgeRef = React.useRef() const facetsRequestsRef = React.useRef({}) const latestFacetRequestRef = React.useRef({}) @@ -304,10 +310,15 @@ const MapProject = () => { React.useEffect(() => { fetchMapTypes() + fetchAIModels() + if(params.projectId && params.owner) { fetchAndSetProject() + return + } + if(copyFromProjectURL) { + copyFromProject() } - fetchAIModels() }, []) React.useEffect(() => { @@ -324,6 +335,27 @@ const MapProject = () => { } }, [repoVersion, project]) + + const copyFromProject = () => { + setLoadingProject(true) + APIService.new().overrideURL(copyFromProjectURL).appendToUrl('configurations/').get().then(response => { + const copiedProject = response.data + setProject(null) + setFilters(copiedProject.filters || {}) + setLookupConfig(copiedProject.lookup_config || {}) + setCandidatesScore(copiedProject.score_configuration || {recommended: 99, available: 70}) + setRetired(Boolean(copiedProject.include_retired || false)) + setAlgosSelected(copiedProject.algorithms || []) + setEncoderModel(copiedProject.encoder_model || DEFAULT_ENCODER_MODEL) + if(copiedProject.target_repo_url) { + const repoParams = URIToParentParams(copiedProject.target_repo_url, true) + fetchRepo(dropVersion(copiedProject.target_repo_url)) + fetchVersions(copiedProject.target_repo_url, repoParams?.repoVersion || 'HEAD') + } + setConfigure(true) + }).finally(() => setLoadingProject(false)) + } + const fetchAndSetProject = () => { setLoadingProject(true) let url = ['', params.ownerType, params.owner, 'map-projects', params.projectId, ''].join('/') @@ -2707,6 +2739,15 @@ const MapProject = () => { /> ) + + const onCopyClick = event => { + event.preventDefault() + event.stopPropagation() + if(project?.url) { + window.open(`/#/map-projects/new?copyFrom=${encodeURIComponent(project.url)}`, '_blank', 'noopener,noreferrer') + } + } + return permissionDenied ? : (
{ @@ -2860,6 +2901,7 @@ const MapProject = () => { isProjectsLogOpen={showProjectLogs} configure={configure} setConfigure={setConfigure} + onCopyClick={onCopyClick} /> }
diff --git a/src/components/map-projects/MapProjects.jsx b/src/components/map-projects/MapProjects.jsx index f511823..76bc7a8 100644 --- a/src/components/map-projects/MapProjects.jsx +++ b/src/components/map-projects/MapProjects.jsx @@ -1,4 +1,7 @@ import React from 'react' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next'; + import moment from 'moment' import reject from 'lodash/reject' import Button from '@mui/material/Button' @@ -12,6 +15,7 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Skeleton from '@mui/material/Skeleton'; import ListItemText from '@mui/material/ListItemText' +import Tooltip from '@mui/material/Tooltip' import AddIcon from '@mui/icons-material/Add' import times from 'lodash/times' import APIService from '../../services/APIService' @@ -19,14 +23,16 @@ import { getCurrentUser } from '../../common/utils' import OwnerIcon from '../common/OwnerIcon' import NoResults from '../search/NoResults'; import MapProjectDeleteConfirmDialog from './MapProjectDeleteConfirmDialog'; -import { useTranslation } from 'react-i18next'; const MapProjects = () => { const { t } = useTranslation(); + const history = useHistory() + const user = getCurrentUser() const [loading, setLoading] = React.useState([]) const [projects, setProjects] = React.useState([]) const [deleteProject, setDeleteProject] = React.useState(null) + const fetchProjects = () => { fetchUserProjects() fetchOrgProjects() @@ -53,6 +59,14 @@ const MapProjects = () => { setDeleteProject(null) } + const onCopyClick = (event, project) => { + event.preventDefault() + event.stopPropagation() + if(project?.url) { + history.push(`/map-projects/new?copyFrom=${encodeURIComponent(project.url)}`) + } + } + const isSplitView = false const loaded = loading.length === 2 return ( @@ -141,10 +155,15 @@ const MapProjects = () => { - - + + diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index a05bc05..a23c37e 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -83,6 +83,7 @@ "draft": "Draft", "highlights": "Highlights", "click_to_copy": "Click to Copy", + "copy": "Copy", "submit": "Submit", "external_id": "External ID", "access_level": "Access Level", @@ -589,7 +590,8 @@ "alternates": "Alternates", "model": "Model", "bridge_source_url": "Bridge Source URL", - "bridge_source_url_description": "The interface terminology to search through for bridge matching" + "bridge_source_url_description": "The interface terminology to search through for bridge matching", + "copy_project": "Creates a new project by copying this project's configuration only. No candidates, decisions, or input files will be copied." }, "app": { "web_version": "Web Version", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index b0029b9..2c91027 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -91,6 +91,7 @@ "draft": "Borrador", "highlights": "Destacados", "click_to_copy": "Clic para copiar", + "copy": "Copiar", "submit": "Enviar", "external_id": "ID externo", "access_level": "Nivel de acceso", @@ -564,7 +565,8 @@ "alternates": "Alternativas", "model": "Modelo", "bridge_source_url": "URL de la fuente de Bridge", - "bridge_source_url_description": "La terminología de interfaz para buscar candidatos de coincidencia" + "bridge_source_url_description": "La terminología de interfaz para buscar candidatos de coincidencia", + "copy_project": "Crea un nuevo proyecto copiando solo la configuración de este proyecto. No se copiarán candidatos, decisiones ni archivos de entrada." }, "app": { "web_version": "Versión Web", diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index b9dcf20..c1e2b48 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -82,6 +82,7 @@ "draft": "草案", "highlights": "亮点", "click_to_copy": "单击复制", + "copy": "复制", "submit": "提交", "external_id": "外部 ID", "access_level": "访问权限级别", @@ -589,7 +590,8 @@ "alternates": "备选", "model": "模型", "bridge_source_url": "桥接源 URL", - "bridge_source_url_description": "用于搜索匹配候选对象的接口术语" + "bridge_source_url_description": "用于搜索匹配候选对象的接口术语", + "copy_project": "通过仅复制此项目的配置来创建一个新项目。不会复制候选项、决策或输入文件。" }, "app": { "web_version": "Web 版本",