From cea22ac19c37980d184d156af78bfbe90ee0e077 Mon Sep 17 00:00:00 2001 From: E Kelen Date: Mon, 11 May 2020 20:45:18 +0200 Subject: [PATCH] feat(front): add anchor links to builds and artifacts (#205) remove unused state items remove invalid feather icon sizes add copy anchor link to clipboard button Add anchor link component and artifact anchor links expand build card if URL bar hash link matches child artifact --- web/package-lock.json | 22 ++++++++ web/package.json | 1 + web/src/constants/index.js | 6 -- web/src/store/ResultStore.js | 7 +-- web/src/ui/components/AnchorLink.js | 34 ++++++++++++ .../ui/components/BuildCard/ArtifactCard.js | 45 ++++++++++++++- web/src/ui/components/BuildCard/BuildCard.js | 55 +++++++++++++++++-- .../ui/components/BuildCard/BuildCard.scss | 31 +++++++++++ web/src/ui/components/BuildList.js | 2 +- .../ui/components/FilterModal/FilterModal.js | 6 -- web/src/ui/components/ShowFiltersButton.js | 2 +- web/src/ui/pages/Home/Home.js | 2 +- 12 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 web/src/ui/components/AnchorLink.js diff --git a/web/package-lock.json b/web/package-lock.json index 91c88ec6..83f92457 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -4123,6 +4123,14 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, + "copy-to-clipboard": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz", + "integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, "copy-webpack-plugin": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", @@ -12988,6 +12996,15 @@ "prop-types": "^15.6.2" } }, + "react-copy-to-clipboard": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", + "integrity": "sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==", + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -15463,6 +15480,11 @@ "repeat-string": "^1.6.1" } }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", diff --git a/web/package.json b/web/package.json index 1f58ae03..96d45b3c 100755 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "dayjs": "^1.8.25", "js-cookie": "^2.2.1", "react": "16.13.1", + "react-copy-to-clipboard": "^5.0.2", "react-dom": "16.13.1", "react-feather": "2.0.3", "react-hook-form": "5.2.0", diff --git a/web/src/constants/index.js b/web/src/constants/index.js index f5c2fb05..9fbe0e3f 100644 --- a/web/src/constants/index.js +++ b/web/src/constants/index.js @@ -32,12 +32,6 @@ export const ARTIFACT_KINDS = Object.values(ARTIFACT_KIND_VALUE).map((kind) => kind.toString() ); -export const PLATFORMS = { - iOS: '1', - android: '2', - none: '3', -}; - export const BRANCH = { MASTER: 'MASTER', }; diff --git a/web/src/store/ResultStore.js b/web/src/store/ResultStore.js index fa9fef66..e423bcf5 100644 --- a/web/src/store/ResultStore.js +++ b/web/src/store/ResultStore.js @@ -1,7 +1,7 @@ import React, {useReducer} from 'react'; import {cloneDeep} from 'lodash'; import {retrieveAuthCookie} from '../api/auth'; -import {actions, PLATFORMS, ARTIFACT_KIND_VALUE} from '../constants'; +import {actions, ARTIFACT_KIND_VALUE} from '../constants'; // TODO: Yes, this file needs a new name, and should maybe be split export const ResultContext = React.createContext(); @@ -18,10 +18,6 @@ export const INITIAL_STATE = { uiFilters: { artifact_kinds: [ARTIFACT_KIND_VALUE.IPA], }, - filtersPlatform: { - iOS: true, - android: false, - }, filtersBranch: { master: false, develop: false, @@ -33,7 +29,6 @@ export const INITIAL_STATE = { }, filtersImplemented: { apps: ['chat'], - os: ['iOS', 'android'], branch: ['all'], }, }; diff --git a/web/src/ui/components/AnchorLink.js b/web/src/ui/components/AnchorLink.js new file mode 100644 index 00000000..e3d27807 --- /dev/null +++ b/web/src/ui/components/AnchorLink.js @@ -0,0 +1,34 @@ +import React from 'react'; +import {CopyToClipboard} from 'react-copy-to-clipboard'; + +const AnchorLink = ({ + tooltipMessage, + setTooltipMessage, + location, + children, + target, +}) => { + return ( + <> +
+ {tooltipMessage && ( +
+ {tooltipMessage} +
+ )} + { + setTooltipMessage('Link copied'); + setTimeout(() => setTooltipMessage(''), 1000); + }} + > + {children} + +
+ + ); +}; + +export default AnchorLink; diff --git a/web/src/ui/components/BuildCard/ArtifactCard.js b/web/src/ui/components/BuildCard/ArtifactCard.js index c2cdba21..6934ecf7 100644 --- a/web/src/ui/components/BuildCard/ArtifactCard.js +++ b/web/src/ui/components/BuildCard/ArtifactCard.js @@ -1,6 +1,12 @@ -import React, {useContext} from 'react'; +import React, {useContext, useState, useRef, useEffect} from 'react'; import {ThemeContext} from '../../../store/ThemeStore'; -import {Clock, Calendar, ArrowDownCircle, AlertTriangle} from 'react-feather'; +import { + Clock, + Calendar, + ArrowDownCircle, + AlertTriangle, + Link, +} from 'react-feather'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faAndroid, faApple} from '@fortawesome/free-brands-svg-icons'; import { @@ -8,22 +14,29 @@ import { faFile, faHammer, } from '@fortawesome/free-solid-svg-icons'; +import {useLocation} from 'react-router-dom'; import {tagStyle, actionButtonStyle} from '../../styleTools/buttonStyler'; +import AnchorLink from '../AnchorLink'; import {KIND_TO_PLATFORM, ARTIFACT_STATE} from '../../../constants'; import {getTimeDuration, getRelativeTime} from '../../../util/date'; import './BuildCard.scss'; +// TODO: Factor out into DOM util file +const scrollToRef = (ref) => window.scrollTo(0, ref.current.offsetTop); + const ArtifactCard = ({ artifact, buildMergeUpdatedAt, mrShortId, buildStartedAt, buildFinishedAt, + hashLinkMatch, }) => { const {theme} = useContext(ThemeContext); + const location = useLocation(); const { id: artifactId = '', state: artifactState = '', @@ -34,6 +47,15 @@ const ArtifactCard = ({ file_size: artifactFileSize = '', driver: artifactDriver = '', } = artifact; + const [tooltipMessage, setTooltipMessage] = useState(''); + const artifactRef = useRef(null); + + const executeScroll = () => scrollToRef(artifactRef); + useEffect(() => { + if (hashLinkMatch === true) { + executeScroll(); + } + }, []); const timeSinceBuildUpdated = getRelativeTime(buildMergeUpdatedAt); const buildDurationSeconds = getTimeDuration(buildStartedAt, buildFinishedAt); @@ -148,13 +170,30 @@ const ArtifactCard = ({ ); + const SharableArtifactLink = ( + } + target={artifactId} + /> + ); + return (
-
{PlatformIcon}
+
+ {SharableArtifactLink} + {PlatformIcon} +
diff --git a/web/src/ui/components/BuildCard/BuildCard.js b/web/src/ui/components/BuildCard/BuildCard.js index 3707820c..12641d5a 100644 --- a/web/src/ui/components/BuildCard/BuildCard.js +++ b/web/src/ui/components/BuildCard/BuildCard.js @@ -1,4 +1,4 @@ -import React, {useContext, useState} from 'react'; +import React, {useContext, useState, useRef, useEffect} from 'react'; import { GitCommit, GitMerge, @@ -8,6 +8,7 @@ import { ChevronDown, AlertCircle, Calendar, + Link, } from 'react-feather'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faGithub} from '@fortawesome/free-brands-svg-icons'; @@ -17,6 +18,7 @@ import { faPencilAlt, faFile, } from '@fortawesome/free-solid-svg-icons'; +import {useLocation} from 'react-router-dom'; import {ThemeContext} from '../../../store/ThemeStore'; import {sharedThemes} from '../../styleTools/themes'; @@ -28,11 +30,23 @@ import {MR_STATE, BUILD_STATE} from '../../../constants'; import {getRelativeTime, getTimeLabel} from '../../../util/date'; import './BuildCard.scss'; +import AnchorLink from '../AnchorLink'; + +const scrollToRef = (ref) => window.scrollTo(0, ref.current.offsetTop); const BuildCard = ({build, toCollapse}) => { const [expanded, toggleExpanded] = useState(!toCollapse); const [messageExpanded, toggleMessageExpanded] = useState(false); + const [tooltipMessage, setTooltipMessage] = useState(''); + const [artifactHashLinkMatch, setContainsArtifactHashLinkMatch] = useState( + '' + ); const {theme} = useContext(ThemeContext); + const location = useLocation(); + const buildRef = useRef(null); + + const executeScroll = () => scrollToRef(buildRef); + const { short_id: buildShortId = '', id: buildId = '', @@ -46,6 +60,7 @@ const BuildCard = ({build, toCollapse}) => { completed_at: buildCompletedAt = '', updated_at: buildUpdatedAt = '', driver: buildDriver = '', + has_mergerequest: buildHasMr = null, has_mergerequest: { short_id: mrShortId = '', commit_url: mrCommitUrl = '', @@ -61,12 +76,29 @@ const BuildCard = ({build, toCollapse}) => { } = {}, } = {}, has_project: {id: buildProjectUrl = ''} = {}, + has_artifacts: buildHasArtifacts = [], } = build || {}; + useEffect(() => { + if (location.hash) { + if (location.hash.slice(1) === buildId) { + executeScroll(); + } else if (buildHasArtifacts.length) { + const artifactMatch = buildHasArtifacts.find( + (a) => a.id === location.hash.slice(1) + ); + if (artifactMatch) { + setContainsArtifactHashLinkMatch(artifactMatch); + toggleExpanded(true); + } + } + } + }, []); + const COMMIT_LEN = 7; const MESSAGE_LEN = 280; const isMaster = buildBranch && buildBranch.toUpperCase() === 'MASTER'; - const {has_mergerequest: buildHasMr = false} = build; + // const {has_mergerequest: buildHasMr = false} = build; const timeSinceUpdated = getRelativeTime(buildUpdatedAt); const timeSinceCreated = getRelativeTime(buildCreatedAt); @@ -155,6 +187,17 @@ const BuildCard = ({build, toCollapse}) => { ); + const SharableLink = ( + } + target={buildId} + /> + ); + const CommitIcon = mrCommitUrl ? ( { }} onClick={() => toggleExpanded(!expanded)} > - {expanded ? : } + {expanded ? : }
); @@ -233,7 +276,7 @@ const BuildCard = ({build, toCollapse}) => {
) : (
- +
); @@ -363,7 +406,7 @@ const BuildCard = ({build, toCollapse}) => { ); return ( -
+
{ key={buildId} >
+ {SharableLink} {CardIcon}

{CardTitle} @@ -429,6 +473,7 @@ const BuildCard = ({build, toCollapse}) => { buildStartedAt={buildStartedAt} buildFinishedAt={buildFinishedAt} key={artifact.id} + artifactHashLinkMatch={artifactHashLinkMatch === artifact.id} /> ))}

diff --git a/web/src/ui/components/BuildCard/BuildCard.scss b/web/src/ui/components/BuildCard/BuildCard.scss index 67b6f098..31f46862 100644 --- a/web/src/ui/components/BuildCard/BuildCard.scss +++ b/web/src/ui/components/BuildCard/BuildCard.scss @@ -23,6 +23,14 @@ } } + div.confirm-copy { + position: absolute; + width: auto; + white-space: nowrap; + opacity: 0.9; + padding: 0.2rem; + } + pre { padding: 0px; margin: 0px; @@ -36,6 +44,29 @@ min-width: 0; } + .copy-build-to-clipboard-icon { + display: none; + @include xs { + display: block; + position: absolute; + top: 0.5rem; + left: 0.5rem; + cursor: pointer; + } + } + + .copy-artifact-to-clipboard-icon { + display: none; + @include xs { + display: block; + position: absolute; + left: 0.5rem; + // top: 0.5rem; + // left: 0.5rem; + cursor: pointer; + } + } + .card div.card-row { display: flex; margin: 0px; diff --git a/web/src/ui/components/BuildList.js b/web/src/ui/components/BuildList.js index beff3957..6aaccced 100644 --- a/web/src/ui/components/BuildList.js +++ b/web/src/ui/components/BuildList.js @@ -15,7 +15,7 @@ const BuildList = ({loaded, builds, collapseCondition}) => {
{builds.map((build, i) => ( { const [selectedBranches] = useState(['all']); const filterSelectedAccent = theme.icon.filterSelected; - const updateLocalOs = ({name}) => { - setSelectedOs([name]); - setLocalPlatformId(PLATFORMS[name]); - }; - const applyFilterButtonColors = { backgroundColor: theme.bg.btnPrimary, border: '1px solid ' + theme.bg.btnPrimary, diff --git a/web/src/ui/components/ShowFiltersButton.js b/web/src/ui/components/ShowFiltersButton.js index b419576a..788b03a9 100644 --- a/web/src/ui/components/ShowFiltersButton.js +++ b/web/src/ui/components/ShowFiltersButton.js @@ -23,7 +23,7 @@ const ShowFiltersButton = ({clickAction, showingFiltersModal}) => { return (
- +
); }; diff --git a/web/src/ui/pages/Home/Home.js b/web/src/ui/pages/Home/Home.js index 0fa6a353..ee950ab8 100644 --- a/web/src/ui/pages/Home/Home.js +++ b/web/src/ui/pages/Home/Home.js @@ -16,7 +16,7 @@ import MessageModal from '../../components/MessageModal/MessageModal'; import {ThemeContext} from '../../../store/ThemeStore'; import {ResultContext} from '../../../store/ResultStore'; -import {PLATFORMS, ARTIFACT_KINDS} from '../../../constants'; +import {ARTIFACT_KINDS} from '../../../constants'; import {getBuildList, validateError} from '../../../api'; import './Home.scss';