diff --git a/frontend/src/languages/english.js b/frontend/src/languages/english.js index 58abee414..08bce0301 100644 --- a/frontend/src/languages/english.js +++ b/frontend/src/languages/english.js @@ -712,6 +712,12 @@ const en = { select: "Select a picture from the gallery or upload a custom picture" }, + clipboard: { + copy: "Copy link", + copied: "Link copied", + paste: "Paste link" + }, + language: { english: "English", french: "French", diff --git a/frontend/src/languages/french.js b/frontend/src/languages/french.js index b881b97e2..2d0fa8898 100644 --- a/frontend/src/languages/french.js +++ b/frontend/src/languages/french.js @@ -722,6 +722,12 @@ const fr = { select: "Sélectionner ou télécharger une image personnalisée" }, + clipboard: { + copy: "Copier le lien", + copied: "Lien copié", + paste: "Coller le lien" + }, + language: { english: "Anglais", french: "Français", diff --git a/frontend/src/languages/georgian.js b/frontend/src/languages/georgian.js index 3f0fbe9ae..d3b6cb0de 100644 --- a/frontend/src/languages/georgian.js +++ b/frontend/src/languages/georgian.js @@ -714,6 +714,12 @@ const ka = { select: "აირჩიეთ სურათი გალერეიდან ან ატვირთეთ მორგებული სურათი." }, + clipboard: { + copy: "დააკოპირეთ ბმული", + copied: "ბმული კოპირებულია", + paste: "ჩასვით ლინკი" + }, + language: { english: "ინგლისური", french: "ფრანგული", diff --git a/frontend/src/languages/german.js b/frontend/src/languages/german.js index 49312740a..2363dd42c 100644 --- a/frontend/src/languages/german.js +++ b/frontend/src/languages/german.js @@ -714,6 +714,12 @@ const de = { select: "Wählen Sie ein Bild aus oder laden Sie ein eigenes hoch" }, + clipboard: { + copy: "Link kopieren", + copied: "Link kopiert", + paste: "Link einfügen" + }, + language: { english: "Englisch", french: "Französisch", diff --git a/frontend/src/languages/portuguese.js b/frontend/src/languages/portuguese.js index b6c18d963..4e85eeb5e 100644 --- a/frontend/src/languages/portuguese.js +++ b/frontend/src/languages/portuguese.js @@ -716,6 +716,12 @@ const pt = { select: "Selecione ou faça o upload de uma imagem personalizada" }, + clipboard: { + copy: "Copiar link", + copied: "Link copiado", + paste: "Colar link" + }, + language: { english: "English", french: "Français", diff --git a/frontend/src/pages/Common/Markdown.js b/frontend/src/pages/Common/Markdown.js index 7d984aa82..e192ce512 100644 --- a/frontend/src/pages/Common/Markdown.js +++ b/frontend/src/pages/Common/Markdown.js @@ -2,39 +2,60 @@ import React from "react"; import { BoldItalicUnderlineToggles, CreateLink, + headingsPlugin, linkDialogPlugin, linkPlugin, listsPlugin, ListsToggle, MDXEditor, + quotePlugin, toolbarPlugin, UndoRedo } from "@mdxeditor/editor"; +import strings from "../../localizeStrings"; + import "@mdxeditor/editor/style.css"; -const Markdown = ({ text, onChangeFunc }) => { +const Markdown = ({ text, onChangeFunc, readOnly, paste, clipboard }) => { + const ref = React.useRef(null); return ( - ( - <> - {" "} - - - - - - ) - }), - linkDialogPlugin(), - listsPlugin(), - linkPlugin() - ]} - /> + <> + {clipboard ? ( + + ) : null} + ( + <> + {" "} + + + + + + ) + }) + ]} + /> + ); }; diff --git a/frontend/src/pages/Navbar/actions.js b/frontend/src/pages/Navbar/actions.js index 125851d7d..09944dfa2 100644 --- a/frontend/src/pages/Navbar/actions.js +++ b/frontend/src/pages/Navbar/actions.js @@ -36,6 +36,9 @@ export const SAVE_EMAIL_ADDRESS_SUCCESS = "SAVE_EMAIL_ADDRESS_SUCCESS"; export const SAVE_EMAIL_ADDRESS_FAILED = "SAVE_EMAIL_ADDRESS_FAILED"; export const SET_VALID_EMAIL_ADDRESS_INPUT = "SET_VALID_EMAIL_ADDRESS_INPUT"; +export const CLIPBOARD_COPY = "CLIPBOARD_COPY"; +export const CLIPBOARD_PASTE = "CLIPBOARD_PASTE"; + export function toggleSidebar() { return { type: TOGGLE_SIDEBAR @@ -144,3 +147,16 @@ export function setValidEmailAddressInput(valid) { valid }; } + +export function clipboardCopy(text) { + return { + type: CLIPBOARD_COPY, + text + }; +} + +export function clipboardPaste() { + return { + type: CLIPBOARD_PASTE + }; +} diff --git a/frontend/src/pages/Navbar/reducer.js b/frontend/src/pages/Navbar/reducer.js index 4c53bcc3f..f1acb3dce 100644 --- a/frontend/src/pages/Navbar/reducer.js +++ b/frontend/src/pages/Navbar/reducer.js @@ -10,6 +10,8 @@ import { import { FETCH_ALL_SUBPROJECT_DETAILS_SUCCESS } from "../Workflows/actions"; import { + CLIPBOARD_COPY, + CLIPBOARD_PASTE, DISABLE_USER_PROFILE_EDIT, ENABLE_USER_PROFILE_EDIT, FETCH_ACTIVE_PEERS_SUCCESS, @@ -40,7 +42,8 @@ const defaultState = fromJS({ userProfileOpen: false, userProfileEdit: false, tempEmailAddress: "", - isEmailAddressInputValid: true + isEmailAddressInputValid: true, + clipboard: null }); export default function navbarReducer(state = defaultState, action) { @@ -100,6 +103,12 @@ export default function navbarReducer(state = defaultState, action) { searchTerm: defaultState.get("searchTerm"), searchBarDisplayed: defaultState.get("searchBarDisplayed") }); + case CLIPBOARD_COPY: { + return state.set("clipboard", action.text || ""); + } + case CLIPBOARD_PASTE: { + return state.set("clipboard", null); + } default: return state; } diff --git a/frontend/src/pages/Overview/ProjectDialogContainer.js b/frontend/src/pages/Overview/ProjectDialogContainer.js index 69894d342..08461c57a 100644 --- a/frontend/src/pages/Overview/ProjectDialogContainer.js +++ b/frontend/src/pages/Overview/ProjectDialogContainer.js @@ -3,6 +3,7 @@ import { connect } from "react-redux"; import { toJS } from "../../helper"; import withInitialLoading from "../Loading/withInitialLoading"; +import { clipboardPaste } from "../Navbar/actions"; import { storeSnackbarMessage } from "../Notifications/actions"; import { @@ -38,7 +39,8 @@ const mapStateToProps = (state) => { currentStep: state.getIn(["overview", "currentStep"]), projectToAdd: state.getIn(["overview", "projectToAdd"]), dialogTitle: state.getIn(["overview", "dialogTitle"]), - allowedIntents: state.getIn(["login", "allowedIntents"]) + allowedIntents: state.getIn(["login", "allowedIntents"]), + clipboard: state.getIn(["navbar", "clipboard"]) }; }; @@ -61,7 +63,8 @@ const mapDispatchToProps = (dispatch) => { addProjectTag: (tag) => dispatch(addProjectTag(tag)), removeProjectTag: (tag) => dispatch(removeProjectTag(tag)), addCustomImage: (imageBase64) => dispatch(addCustomImage(imageBase64)), - removeCustomImage: (imageBase64) => dispatch(removeCustomImage(imageBase64)) + removeCustomImage: (imageBase64) => dispatch(removeCustomImage(imageBase64)), + pasteClipboard: () => dispatch(clipboardPaste()) }; }; diff --git a/frontend/src/pages/Overview/ProjectDialogContent.js b/frontend/src/pages/Overview/ProjectDialogContent.js index 2064868cf..40378aeff 100644 --- a/frontend/src/pages/Overview/ProjectDialogContent.js +++ b/frontend/src/pages/Overview/ProjectDialogContent.js @@ -55,7 +55,13 @@ const ProjectDialogContent = (props) => {
{strings.project.markdown} - +
); diff --git a/frontend/src/pages/SubProjects/SubProjectContainer.js b/frontend/src/pages/SubProjects/SubProjectContainer.js index 1f5b3c29b..adb221bf6 100644 --- a/frontend/src/pages/SubProjects/SubProjectContainer.js +++ b/frontend/src/pages/SubProjects/SubProjectContainer.js @@ -4,6 +4,8 @@ import _isEqual from "lodash/isEqual"; import queryString from "query-string"; import ContentAdd from "@mui/icons-material/Add"; +import ContentPasteIcon from "@mui/icons-material/ContentPaste"; +import { IconButton } from "@mui/material"; import { Fab, Typography } from "@mui/material"; import { toJS } from "../../helper"; @@ -17,8 +19,8 @@ import AdditionalInfo from "../Common/AdditionalInfo"; import worker from "../Common/filterProjects.worker.js"; import LiveUpdates from "../LiveUpdates/LiveUpdates"; import { fetchUser } from "../Login/actions"; -import { setSelectedView } from "../Navbar/actions"; -import { hideHistory, openHistory } from "../Notifications/actions"; +import { clipboardCopy, setSelectedView } from "../Navbar/actions"; +import { hideHistory, openHistory, showSnackbar, storeSnackbarMessage } from "../Notifications/actions"; import { closeProject, @@ -77,6 +79,16 @@ class SubProjectContainer extends Component { } componentDidUpdate(prevProps) { + if (this.props.router.location.pathname !== prevProps.router.location.pathname) { + const newProjectId = this.props.router.location.pathname.split("/")[2]; + if (newProjectId !== this.projectId) { + this.setState({ isDataFetched: false }); + this.projectId = newProjectId; + this.props.setSelectedView(this.projectId, "project"); + this.props.fetchAllProjectDetails(this.projectId, true); + this.setState({ isDataFetched: true }); + } + } const searchTermChanges = this.props.searchTerm !== prevProps.searchTerm; const projectsChange = !_isEqual(this.props.subProjects, prevProps.subProjects); @@ -127,6 +139,19 @@ class SubProjectContainer extends Component {
) : (
+ <> + { + this.props.clipboardCopy(`[${this.props.projectName}](${this.props.router.location.pathname})`); + this.props.storeSnackbarMessage(strings.clipboard.copied); + this.props.showSnackbar(); + }} + data-test={`project-copy-button-${projectId}`} + size="small" + > + + + { storeSubSearchTerm: (subSearchTerm) => dispatch(storeSubSearchTerm(subSearchTerm)), storeSubSearchBarDisplayed: (subSearchBarDisplayed) => dispatch(storeSubSearchBarDisplayed(subSearchBarDisplayed)), storeFilteredSubProjects: (filteredSubProjects) => dispatch(storeFilteredSubProjects(filteredSubProjects)), - storeSubSearchTermArray: (searchTerms) => dispatch(storeSubSearchTermArray(searchTerms)) + storeSubSearchTermArray: (searchTerms) => dispatch(storeSubSearchTermArray(searchTerms)), + clipboardCopy: (text) => dispatch(clipboardCopy(text)), + showSnackbar: () => dispatch(showSnackbar()), + storeSnackbarMessage: (message) => dispatch(storeSnackbarMessage(message)) }; }; @@ -221,7 +249,8 @@ const mapStateToProps = (state) => { searchTerms: state.getIn(["detailview", "searchTerms"]), idsPermissionsUnassigned: state.getIn(["detailview", "idsPermissionsUnassigned"]), isDataLoading: state.getIn(["loading", "loadingVisible"]), - isLiveUpdatesProjectEnabled: state.getIn(["detailview", "isLiveUpdatesProjectEnabled"]) + isLiveUpdatesProjectEnabled: state.getIn(["detailview", "isLiveUpdatesProjectEnabled"]), + clipboard: state.getIn(["navbar", "clipboard"]) }; }; diff --git a/frontend/src/pages/Workflows/SubProjectDetails.js b/frontend/src/pages/Workflows/SubProjectDetails.js index cb81f5541..8ba317075 100644 --- a/frontend/src/pages/Workflows/SubProjectDetails.js +++ b/frontend/src/pages/Workflows/SubProjectDetails.js @@ -1,9 +1,11 @@ import React from "react"; +import { useLocation } from "react-router-dom"; import _isEmpty from "lodash/isEmpty"; import AmountIcon from "@mui/icons-material/AccountBalance"; import BarChartIcon from "@mui/icons-material/BarChart"; import DoneIcon from "@mui/icons-material/Check"; +import ContentPasteIcon from "@mui/icons-material/ContentPaste"; import DateIcon from "@mui/icons-material/DateRange"; import AssigneeIcon from "@mui/icons-material/Group"; import PersonIcon from "@mui/icons-material/Person"; @@ -24,7 +26,14 @@ import TableRow from "@mui/material/TableRow"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import { statusIconMapping, statusMapping, toAmountString, toCurrencyCode, trimSpecialChars, unixTsToString } from "../../helper.js"; +import { + statusIconMapping, + statusMapping, + toAmountString, + toCurrencyCode, + trimSpecialChars, + unixTsToString +} from "../../helper.js"; import strings from "../../localizeStrings"; import SubProjectAnalyticsDialog from "../Analytics/SubProjectAnalyticsDialog"; import BudgetEmptyState from "../Common/BudgetEmptyState"; @@ -66,165 +75,183 @@ const SubProjectDetails = ({ openAnalyticsDialog, projectedBudgets, subprojectValidator, - fixedWorkflowitemType + fixedWorkflowitemType, + clipboardCopy, + storeSnackbarMessage, + showSnackbar }) => { const mappedStatus = statusMapping(status); const statusIcon = statusIconMapping[status]; const date = unixTsToString(created); const validator = users.find((user) => user.id === subprojectValidator); + const location = useLocation(); + const closingOfSubProjectAllowed = subProjectCanBeClosed(status === "closed", canCloseSubproject, workflowItems); return ( -
- - - - {displayName ? ( + <> + { + clipboardCopy(`[${displayName}](${location.pathname})`); + storeSnackbarMessage(strings.clipboard.copied); + showSnackbar(); + }} + data-test={`subproject-copy-button-${subprojectId}`} + size="small" + > + + +
+ + + + {displayName ? ( + + {displayName[0]} + + ) : null} + + + - {displayName[0]} + + + - ) : null} - - - - - - - - - - - - - - - - - - - {!_isEmpty(fixedWorkflowitemType) ? ( + + - + - + - ) : null} - + {!_isEmpty(fixedWorkflowitemType) ? ( + + + + + + + + + ) : null} + -
- {strings.common.projected_budget} - {isDataLoading ? ( -
- ) : projectedBudgets.length > 0 ? ( -
- - - - {strings.common.organization} - - {strings.common.amount} - - - {strings.common.currency} - - - - - {projectedBudgets.map((budget) => ( - - {budget.organization} +
+ {strings.common.projected_budget} + {isDataLoading ? ( +
+ ) : projectedBudgets.length > 0 ? ( +
+
+ + + {strings.common.organization} - {toAmountString(budget.value, undefined, true)} + {strings.common.amount} - {toCurrencyCode(budget.value, budget.currencyCode, true)} + {strings.common.currency} - ))} - -
-
- -
-
- ) : ( - - )} -
- - - - {statusIcon} - - - {status !== "closed" ? ( - -
- + + {projectedBudgets.map((budget) => ( + + {budget.organization} + + {toAmountString(budget.value, undefined, true)} + + + {toCurrencyCode(budget.value, budget.currencyCode, true)} + + + ))} + + +
+
- - ) : null} - - - - - - - - - } - secondary={strings.subproject.assignee} - /> - - {!_isEmpty(validator) ? ( +
+ ) : ( + + )} +
+ + + + {statusIcon} + + + {status !== "closed" ? ( + +
+ + + +
+
+ ) : null} +
- + - + + } + secondary={strings.subproject.assignee} + /> - ) : null} -
-
- -
+ {!_isEmpty(validator) ? ( + + + + + + + + + ) : null} +
+
+ +
+ ); }; export default SubProjectDetails; diff --git a/frontend/src/pages/Workflows/WorkflowContainer.js b/frontend/src/pages/Workflows/WorkflowContainer.js index 615da2388..b13b694cd 100644 --- a/frontend/src/pages/Workflows/WorkflowContainer.js +++ b/frontend/src/pages/Workflows/WorkflowContainer.js @@ -20,8 +20,8 @@ import InformationDialog from "../Common/InformationDialog"; import { addDocument } from "../Documents/actions"; import LiveUpdates from "../LiveUpdates/LiveUpdates"; import { fetchUser } from "../Login/actions"; -import { setSelectedView } from "../Navbar/actions"; -import { openHistory } from "../Notifications/actions"; +import { clipboardCopy, setSelectedView } from "../Navbar/actions"; +import { openHistory, showSnackbar, storeSnackbarMessage } from "../Notifications/actions"; import SubProjectHistoryDrawer from "../SubProjects/SubProjectHistoryDrawer"; import { @@ -173,6 +173,9 @@ class WorkflowContainer extends Component { closeSubproject={this.closeSubproject} canCloseSubproject={canCloseSubproject} isDataLoading={this.props.isDataLoading} + clipboardCopy={this.props.clipboardCopy} + storeSnackbarMessage={this.props.storeSnackbarMessage} + showSnackbar={this.props.showSnackbar} /> {this.props.permissionDialogShown ? ( @@ -291,7 +294,10 @@ const mapDispatchToProps = (dispatch, _ownProps) => { disableLiveUpdatesSubproject: () => dispatch(disableLiveUpdatesSubproject()), storeWorkflowSearchBarDisplayed: (workflowSearchBarDisplayed) => dispatch(storeWorkflowSearchBarDisplayed(workflowSearchBarDisplayed)), - storeWorkflowSearchTermArray: (searchTerms) => dispatch(storeWorkflowSearchTermArray(searchTerms)) + storeWorkflowSearchTermArray: (searchTerms) => dispatch(storeWorkflowSearchTermArray(searchTerms)), + clipboardCopy: (text) => dispatch(clipboardCopy(text)), + showSnackbar: () => dispatch(showSnackbar()), + storeSnackbarMessage: (message) => dispatch(storeSnackbarMessage(message)) }; }; @@ -301,6 +307,7 @@ const mapStateToProps = (state) => { amount: state.getIn(["workflow", "amount"]), assignee: state.getIn(["workflow", "assignee"]), budgetEditEnabled: state.getIn(["workflow", "subProjectBudgetEditEnabled"]), + clipboard: state.getIn(["navbar", "clipboard"]), created: state.getIn(["workflow", "created"]), currency: state.getIn(["workflow", "currency"]), currentUser: state.getIn(["login", "id"]), @@ -322,8 +329,8 @@ const mapStateToProps = (state) => { permissionDialogShown: state.getIn(["workflow", "showWorkflowPermissions"]), projectedBudgets: state.getIn(["workflow", "projectedBudgets"]), rejectReason: state.getIn(["workflow", "rejectReason"]), - searchTerm: state.getIn(["workflow", "searchTerm"]), searchBarDisplayed: state.getIn(["workflow", "searchBarDisplayed"]), + searchTerm: state.getIn(["workflow", "searchTerm"]), searchTerms: state.getIn(["workflow", "searchTerms"]), selectedWorkflowItems: state.getIn(["workflow", "selectedWorkflowItems"]), showDetailsItem: state.getIn(["workflow", "showDetailsItem"]), @@ -338,9 +345,9 @@ const mapStateToProps = (state) => { worflowDetailsInitialTab: state.getIn(["workflow", "worflowDetailsInitialTab"]), workflowDocuments: state.getIn(["documents", "tempDocuments"]), workflowItems: state.getIn(["workflow", "workflowItems"]), - workflowMode: state.getIn(["workflow", "workflowMode"]), workflowItemsBeforeSort: state.getIn(["workflow", "workflowItemsBeforeSort"]), workflowitemsBulkAction: state.getIn(["workflow", "workflowitemsBulkAction"]), + workflowMode: state.getIn(["workflow", "workflowMode"]), workflowSortEnabled: state.getIn(["workflow", "workflowSortEnabled"]) }; };