diff --git a/docker-compose.yml b/docker-compose.yml index 8f6c968bb..aa695f001 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -149,8 +149,8 @@ services: - CONTAINER_EXPIRE_SECS=3600 - IDLE_TIMEOUT=5 - - BROWSER_NET=webrecorder_browsers - - MAIN_NET=webrecorder_default + - BROWSER_NET=conifer_browsers + - MAIN_NET=conifer_default ports: - 9020:9020 @@ -190,11 +190,12 @@ services: volumes: - ./data/solr:/var/solr + - ./solrconf:/opt/solr/server/solr/configsets/solrconf - entrypoint: - - docker-entrypoint.sh + command: - solr-precreate - - webrecorder + - conifer + - /opt/solr/server/solr/configsets/solrconf ports: - 8983:8983 @@ -227,4 +228,3 @@ networks: browsers: driver: bridge - diff --git a/frontend/babel.config.js b/frontend/babel.config.js index f495ebb7c..ce0786f0d 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -16,6 +16,7 @@ module.exports = { ], "plugins": [ ["@babel/plugin-proposal-class-properties", { "loose": true }], + ["@babel/plugin-proposal-private-methods", { "loose": true }], "@babel/plugin-proposal-export-default-from", "@babel/plugin-transform-runtime", "add-module-exports", diff --git a/frontend/package.json b/frontend/package.json index 1d729039d..a73eca695 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,6 +75,7 @@ "react-dnd": "^6.0.0", "react-dnd-html5-backend": "^6.0.0", "react-dom": "^16.9.0", + "react-google-recaptcha": "^2.1.0", "react-helmet": "^6.0.0", "react-hot-loader": "^4.6.3", "react-redux": "^5.1.1", @@ -97,7 +98,7 @@ "resolve-url-loader": "^2.3.0", "sass-loader": "^7.1.0", "serialize-javascript": "^1.5.0", - "shepherd-client": "github:oldweb-today/shepherd-client#1.2.1", + "shepherd-client": "github:oldweb-today/shepherd-client#rb-dimensions", "strip-loader": "^0.1.2", "style-loader": "0.23.1", "superagent": "^4.0.0", diff --git a/frontend/src/components/RemoveWidget/index.js b/frontend/src/components/RemoveWidget/index.js index fff6ff9bc..050c85986 100644 --- a/frontend/src/components/RemoveWidget/index.js +++ b/frontend/src/components/RemoveWidget/index.js @@ -41,11 +41,23 @@ class RemoveWidget extends Component { withConfirmation: true }; + static getDerivedStateFromProps(props, state) { + if (!props.isDeleting && state.isDeleting && state.confirmRemove) { + return { + confirmRemove: false, + isDeleting: false + }; + } + + return null; + } + constructor(props) { super(props); this.state = { - confirmRemove: false + confirmRemove: false, + isDeleting: false }; } @@ -58,12 +70,6 @@ class RemoveWidget extends Component { return false; } - componentDidUpdate(prevProps) { - if (!this.props.isDeleting && prevProps.isDeleting) { - this.setState({ confirmRemove: false }); - } - } - removeClick = (evt) => { evt.stopPropagation(); @@ -73,6 +79,7 @@ class RemoveWidget extends Component { return; } + this.setState({ isDeleting: true }); this.props.callback(); } else { this.setState({ confirmRemove: true }); diff --git a/frontend/src/components/Searchbox/index.js b/frontend/src/components/Searchbox/index.js index 210a35f72..f3010a593 100644 --- a/frontend/src/components/Searchbox/index.js +++ b/frontend/src/components/Searchbox/index.js @@ -12,6 +12,7 @@ import './style.scss'; const parseQuery = (search) => { + search = search.trim(); const filters = search.match(/((is|start|end|session):[a-z0-9-.:]+)/ig) || []; const urlFragRX = search.match(/url:((?:https?:\/\/)?(?:www\.)?(?:[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6})?(?:[-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*))/i); const searchRX = search.match(/(?:(?:(?:is|start|end|session|url):[^ ]+\s?)+\s?)?(.*)/i); @@ -38,7 +39,7 @@ class Searchbox extends PureComponent { clear: PropTypes.func, location: PropTypes.object, search: PropTypes.func, - searching: PropTypes.bool, + busy: PropTypes.bool, searched: PropTypes.bool, }; @@ -46,6 +47,7 @@ class Searchbox extends PureComponent { const modalClosed = !state.options && !state.reset; const textChange = state.search !== state.prevSearch && !state.reset; const filterValues = { + includeAll: state.includeAll, includeWebpages: state.includeWebpages, includeImages: state.includeImages, includeAudio: state.includeAudio, @@ -53,6 +55,7 @@ class Searchbox extends PureComponent { includeDocuments: state.includeDocuments, }; const filterFields = { + includeAll: 'is:any', includeWebpages: 'is:page', includeImages: 'is:image', includeAudio: 'is:audio', @@ -85,13 +88,19 @@ class Searchbox extends PureComponent { searchStruct += b ? `${filterFields[val]} ` : ''; }); + // if no filters set, default to webpage + const filtersSet = Object.values(filterValues).some(f => !!f); + if (!filtersSet) { + filterValues.includeWebpages = true; + } + if (urlFrag || (urlFragTxt && textChange)) { if (textChange) { urlFrag = urlFragTxt; } if (urlFrag) { - searchStruct += textChange ? `url:${urlFragTxt} ` : `url:${encodeURIComponent(urlFrag)} `; + searchStruct += textChange ? `url:${urlFragTxt} ` : `url:${urlFrag} `; } } @@ -164,6 +173,7 @@ class Searchbox extends PureComponent { constructor(props) { super(props); + let includeAll = false; let includeWebpages = true; let includeImages = false; let includeAudio = false; @@ -172,14 +182,17 @@ class Searchbox extends PureComponent { let session = ''; let search = ''; let searchFrag = ''; + let urlFrag = ''; let date = 'anytime'; let startDate = new Date(); let endDate = new Date(); + let urlSearch = false; // create clone this.initialValues = { date, endDate, + includeAll, includeWebpages, includeImages, includeAudio, @@ -188,19 +201,23 @@ class Searchbox extends PureComponent { search, searchFrag, session, - startDate + startDate, + urlFrag, + urlSearch }; if (props.location.search) { const qs = querystring.parse(props.location.search.replace(/^\?/, '')); - if (qs.search) { + if (qs.search || qs.url) { props.search(qs); search = qs.search; searchFrag = qs.search; + urlFrag = qs.url ? decodeURIComponent(qs.url) : ''; } if (qs.mime) { + includeAll = qs.mime.includes('*'); includeWebpages = qs.mime.includes('text/html'); includeImages = qs.mime.includes('image/'); includeAudio = qs.mime.includes('audio/'); @@ -218,12 +235,17 @@ class Searchbox extends PureComponent { endDate = qs.to ? this.parseDate(qs.to) : endDate; date = 'daterange'; } + + if (qs.method === 'url') { + urlSearch = true; + } } this.state = { date, endDate, options: false, + includeAll, includeWebpages, includeImages, includeAudio, @@ -233,7 +255,9 @@ class Searchbox extends PureComponent { search, searchStruct: '', session, - startDate + startDate, + urlFrag, + urlSearch }; if (props.location.search) { @@ -250,7 +274,7 @@ class Searchbox extends PureComponent { componentDidUpdate(prevProps, prevState) { // check for searched prop being cleared if (prevProps.searched && !this.props.searched) { - this.setState({ search: 'is:page', searchFrag: '', urlFrag: '' }); + this.setState({ search: 'is:page ', searchFrag: '', urlFrag: '' }); } } @@ -276,15 +300,24 @@ class Searchbox extends PureComponent { handleChange = (evt) => { // noop while indexing - if (this.props.searching) { + if (this.props.busy) { return; } if (evt.target.type === 'checkbox') { - if (evt.target.name in this.state) { - this.setState({ [evt.target.name]: !this.state[evt.target.name] }); + if (evt.target.name === 'includeAll') { + this.setState({ + includeAll: true, + includeWebpages: false, + includeImages: false, + includeAudio: false, + includeVideo: false, + includeDocuments: false + }); + } else if (evt.target.name in this.state) { + this.setState({ [evt.target.name]: !this.state[evt.target.name], includeAll: false }); } else { - this.setState({ [evt.target.name]: true }); + this.setState({ [evt.target.name]: true, includeAll: false }); } } else { this.setState({ @@ -311,6 +344,7 @@ class Searchbox extends PureComponent { const { date, endDate, + includeAll, includeAudio, includeDocuments, includeImages, @@ -319,15 +353,17 @@ class Searchbox extends PureComponent { search, session, startDate, - urlFrag + urlFrag, + urlSearch } = this.state; const { query } = parseQuery(search); - const mime = (includeWebpages ? 'text/html,' : '') + - (includeImages ? 'image/,' : '') + - (includeAudio ? 'audio/,' : '') + - (includeVideo ? 'video/,' : '') + + const mime = (includeAll ? '*,' : '') + + (includeWebpages ? 'text/html,' : '') + + (includeImages ? 'image/*,' : '') + + (includeAudio ? 'audio/*,' : '') + + (includeVideo ? 'video/*,' : '') + (includeDocuments ? 'application/pdf' : ''); let dateFilter = {}; @@ -343,7 +379,7 @@ class Searchbox extends PureComponent { const urlQuery = {}; if (urlFrag) { - urlQuery.url = urlFrag; + urlQuery.url = encodeURIComponent(urlFrag); } const searchParams = { @@ -361,7 +397,7 @@ class Searchbox extends PureComponent { collection.get('owner'), collection.get('id'), searchParams, - collection.get('autoindexed') + collection.get('autoindexed') && !urlSearch ); // close adv search @@ -382,12 +418,14 @@ class Searchbox extends PureComponent { } render() { - const { collection, searching, searched } = this.props; + const { busy, collection, searched } = this.props; const { date } = this.state; + const busyAction = searched ? 'Searching...' : 'Indexing collection...'; + const inputTitle = busy ? busyAction : 'Search'; return (
- + @@ -396,10 +434,10 @@ class Searchbox extends PureComponent { { - (searching || searched) && + (busy || searched) && { - searching ? + busy ? : } @@ -421,6 +459,7 @@ class Searchbox extends PureComponent {
Include File Types
    +
  • diff --git a/frontend/src/components/collection/CollectionCoverUI/index.js b/frontend/src/components/collection/CollectionCoverUI/index.js index b8784ffdc..00db0b145 100644 --- a/frontend/src/components/collection/CollectionCoverUI/index.js +++ b/frontend/src/components/collection/CollectionCoverUI/index.js @@ -172,6 +172,7 @@ class CollectionCoverUI extends Component { {`${collection.get('title')} (Web archive collection by ${collection.get('owner')})`} : {collection.get('title')} } + diff --git a/frontend/src/components/collection/CollectionFiltersUI/index.js b/frontend/src/components/collection/CollectionFiltersUI/index.js index 310d34960..17b844b28 100644 --- a/frontend/src/components/collection/CollectionFiltersUI/index.js +++ b/frontend/src/components/collection/CollectionFiltersUI/index.js @@ -1,15 +1,21 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; +import { AccessContext } from 'store/contexts'; + import Searchbox from 'components/Searchbox'; class CollectionFiltersUI extends PureComponent { + static contextType = AccessContext; + static propTypes = { + clearIndexingState: PropTypes.func, clearSearch: PropTypes.func, collection: PropTypes.object, disabled: PropTypes.bool, history: PropTypes.object, + loadMeta: PropTypes.func, location: PropTypes.object, searching: PropTypes.bool, searched: PropTypes.bool, @@ -19,13 +25,46 @@ class CollectionFiltersUI extends PureComponent { constructor(props) { super(props); + this.count = 0; + + if (props.collection.get('indexing')) { + this.interval = setInterval(() => { + if (this.count++ > 60) { + clearInterval(this.interval); + props.clearIndexingState(); + } else { + props.loadMeta(props.user.get('username'), props.collection.get('id')); + } + }, 1000); + } + } + + componentDidUpdate(prevProps) { + const { collection, indexing, loadMeta, user } = this.props; + + if (!prevProps.indexing && indexing) { + this.count = 0; + this.interval = setInterval(() => { + if (this.count++ > 240) { + clearInterval(this.interval); + this.props.clearIndexingState(); + } else { + loadMeta(user.get('username'), collection.get('id')); + } + }, 1000); + } + + if (prevProps.indexing && !indexing) { + clearInterval(this.interval); + } + } - this.indexed = false; + componentWillUnmount() { + clearInterval(this.interval); } search = (user, coll, params, fullText) => { - const newSearch = this.context.canAdmin && ['admin', 'beta-archivist'].includes(this.props.user.get('role')) - this.props.searchCollection(user, coll, params, fullText && newSearch); + this.props.searchCollection(user, coll, params, fullText && this.context.canAdmin); } render() { @@ -38,9 +77,13 @@ class CollectionFiltersUI extends PureComponent { location={this.props.location} search={this.search} clear={this.props.clearSearch} - searching={this.props.searching} + busy={this.props.searching || this.props.indexing} searched={this.props.searched} /> + { + this.props.searched && this.props.collection.get('pages').size === 5000 && +
    This query returned over 5k results, try narrowing it with the search filters...
    + }
); } diff --git a/frontend/src/components/collection/CollectionListUI/index.js b/frontend/src/components/collection/CollectionListUI/index.js index effe68670..00d37b752 100644 --- a/frontend/src/components/collection/CollectionListUI/index.js +++ b/frontend/src/components/collection/CollectionListUI/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import classNames from 'classnames'; import { fromJS } from 'immutable'; +import { Link } from 'react-router-dom'; import { Button, Col, Row } from 'react-bootstrap'; import { appHost, tagline } from 'config'; @@ -18,7 +19,7 @@ import RedirectWithStatus from 'components/RedirectWithStatus'; import WYSIWYG from 'components/WYSIWYG'; import { NewCollection } from 'components/siteComponents'; import { Upload } from 'containers'; -import { LinkIcon, PlusIcon, UploadIcon, UserIcon } from 'components/icons'; +import { GearIcon, LinkIcon, PlusIcon, UploadIcon, UserIcon } from 'components/icons'; import CollectionItem from './CollectionItem'; import './style.scss'; @@ -107,6 +108,7 @@ class CollectionListUI extends Component { {`${displayName}'s Collections`} + @@ -124,7 +126,13 @@ class CollectionListUI extends Component { success={this.props.edited}>

{displayName}

-

{ userParam }

+

+ { userParam } + { + auth.getIn(['user', 'role']) == 'admin' && + + } +

{ (user.get('display_url') || canAdmin) && o.get('id') === selectedBk) : false; + const bk = selectedBk ? list.get('bookmarks').find(o => o.get('id') === selectedBk) : null; const pg = bk ? bk.get('page') : collection.get('pages').find(p => p.get('id') === selectedPage); const selectedIndex = selectedBk ? list.get('bookmarks').findIndex(o => o.get('id') === selectedBk) : null; @@ -101,6 +102,13 @@ class InspectorPanelUI extends PureComponent { success={bkEdited} /> } + +

Access Browser

+ { + !__PLAYER__ ? + : + {pg.get('browser', 'Current Browser')} + } } { diff --git a/frontend/src/components/collection/InspectorPanelUI/style.scss b/frontend/src/components/collection/InspectorPanelUI/style.scss index 2948d7f43..0a3aa4a41 100644 --- a/frontend/src/components/collection/InspectorPanelUI/style.scss +++ b/frontend/src/components/collection/InspectorPanelUI/style.scss @@ -61,6 +61,10 @@ word-wrap: break-word; width: 100%; } + + .wr-editor { + margin-bottom: rem(2rem); + } } .page-metadata { diff --git a/frontend/src/components/collection/ListsUI/index.js b/frontend/src/components/collection/ListsUI/index.js index 49fd6836e..a38f9fcd9 100644 --- a/frontend/src/components/collection/ListsUI/index.js +++ b/frontend/src/components/collection/ListsUI/index.js @@ -162,11 +162,12 @@ class ListsUI extends Component { const pagesToAdd = []; /* eslint-disable */ for(const pgIdx of pageSelection) { - pagesToAdd.push( - itemType === draggableTypes.PAGE_ITEM ? - pages.get(pgIdx).toJS() : - pages.get(pgIdx).filterNot(keyIn('id', 'page')).toJS() - ); + const bulkPage = itemType === draggableTypes.PAGE_ITEM ? + pages.get(pgIdx).toJS() : + pages.get(pgIdx).filterNot(keyIn('id', 'page')).toJS(); + + bulkPage.page_id = bulkPage.id; + pagesToAdd.push(bulkPage); } /* eslint-enable */ this.props.bulkAddToList(collection.get('owner'), collection.get('id'), list, pagesToAdd); @@ -284,12 +285,11 @@ class ListsUI extends Component { footer={} dialogClassName="lists-edit-modal">
- - + { created ? : - + }
    diff --git a/frontend/src/components/collection/ListsUI/style.scss b/frontend/src/components/collection/ListsUI/style.scss index 748d1b414..cb5b82833 100644 --- a/frontend/src/components/collection/ListsUI/style.scss +++ b/frontend/src/components/collection/ListsUI/style.scss @@ -282,6 +282,7 @@ input { flex-grow: 1; + margin: 0 rem(.5rem); } } diff --git a/frontend/src/components/collection/TableRenderer/columns.js b/frontend/src/components/collection/TableRenderer/columns.js index bc42bede7..bc2c60fd6 100644 --- a/frontend/src/components/collection/TableRenderer/columns.js +++ b/frontend/src/components/collection/TableRenderer/columns.js @@ -1,9 +1,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { List } from 'immutable'; // import defaultHeaderRenderer from 'react-virtualized/dist/commonjs/Table/defaultHeaderRenderer'; import SortDirection from 'react-virtualized/dist/commonjs/Table/SortDirection'; import classNames from 'classnames'; import { DropTarget, DragSource } from 'react-dnd'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; import { draggableTypes, untitledEntry } from 'config'; import { capitalize, getCollectionLink, getListLink, remoteBrowserMod, stopPropagation } from 'helpers/utils'; @@ -93,8 +95,8 @@ export function BrowserRenderer({ cellData, columnData: { browsers } }) { export function LinkRenderer({ cellData, rowData, columnData: { collection, list } }) { const linkTo = list ? - `${getListLink(collection, list)}/b${rowData.get('id')}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'))}/${rowData.get('url')}` : - `${getCollectionLink(collection)}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'))}/${rowData.get('url')}`; + `${getListLink(collection, list)}/b${rowData.get('id')}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'), '/')}${rowData.get('url')}` : + `${getCollectionLink(collection)}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'), '/')}${rowData.get('url')}`; return ( + +
    + { + results.map((res) => { + return

    ; + }) + } +

    +
    + + ); + return ( + +
    {results.size} match{results.size === 1 ? '' : 'es'}
    +
    + ); +} + + export function TimestampRenderer({ cellData }) { return ; } @@ -140,8 +169,8 @@ export function TimestampRenderer({ cellData }) { export function TitleRenderer({ cellData, rowData, columnData: { collection, list } }) { const linkTo = list ? - `${getListLink(collection, list)}/b${rowData.get('id')}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'))}/${rowData.get('url')}` : - `${getCollectionLink(collection)}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'))}/${rowData.get('url')}`; + `${getListLink(collection, list)}/b${rowData.get('id')}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'), '/')}${rowData.get('url')}` : + `${getCollectionLink(collection)}/${remoteBrowserMod(rowData.get('browser'), rowData.get('timestamp'), '/')}${rowData.get('url')}`; return ( { - if (__PLAYER__ || ['remove', 'id'].includes(props.dataKey)) { + if (__PLAYER__ || ['remove', 'id', 'matched'].includes(props.dataKey)) { return ; } @@ -251,6 +258,8 @@ class TableRenderer extends Component { const columnDefs = this.getColumnDefs(activeList, collection, browsers, list, objectLabel); const sorted = activeList && list.getIn(['sortBy', 'sort']) !== null; + const columns = collection.get('searched') && collection.get('autoindexed') && canAdmin ? ['search', ...this.state.columns] : this.state.columns; + return (
    { @@ -318,7 +327,7 @@ class TableRenderer extends Component { sortBy={sortStore.getIn(['sortBy', 'sort'])} sortDirection={sortStore.getIn(['sortBy', 'dir'])}> { - this.state.columns.map((c, idx) => { + columns.map((c, idx) => { let props = columnDefs[c]; let collData = {}; diff --git a/frontend/src/components/collection/TableRenderer/style.scss b/frontend/src/components/collection/TableRenderer/style.scss index e932531e0..fffa53880 100644 --- a/frontend/src/components/collection/TableRenderer/style.scss +++ b/frontend/src/components/collection/TableRenderer/style.scss @@ -1,5 +1,11 @@ @import 'src/vars'; +.search-results { + em { + background: yellow; + } +} + .table-container { display: flex; flex-direction: column; @@ -40,6 +46,11 @@ margin-right: rem(.5rem); } } + + .big-query-warning { + font-style: italic; + color: $gray700; + } } } @@ -136,6 +147,11 @@ } } + .results-trigger { + padding: rem(1.5rem) 0; + text-align: center; + } + .wr-remove-widget { text-align: center; opacity: 0; diff --git a/frontend/src/components/controls/AutopilotUI/index.js b/frontend/src/components/controls/AutopilotUI/index.js index e0a8cf220..b0ab57638 100644 --- a/frontend/src/components/controls/AutopilotUI/index.js +++ b/frontend/src/components/controls/AutopilotUI/index.js @@ -217,7 +217,7 @@ class AutopilotUI extends Component {
    Best Practices

    - Learn more about how to achieve the best results when using Autopilot capture in this user guide + Learn more about how to achieve the best results when using Autopilot capture in this user guide

    } diff --git a/frontend/src/components/controls/InlineBrowserSelectUI/index.js b/frontend/src/components/controls/InlineBrowserSelectUI/index.js new file mode 100644 index 000000000..b87d27eaf --- /dev/null +++ b/frontend/src/components/controls/InlineBrowserSelectUI/index.js @@ -0,0 +1,79 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Col, Dropdown, Row } from 'react-bootstrap'; + +import { filterBrowsers } from 'config'; + + +class InlineBrowserSelectUI extends PureComponent { + static propTypes = { + accessed: PropTypes.number, + bookmark: PropTypes.object, + browsers: PropTypes.object, + collection: PropTypes.object, + editBk: PropTypes.func, + getBrowsers: PropTypes.func, + list: PropTypes.object, + }; + + getRemoteBrowsers = () => { + if (!this.props.browsers) { + this.props.getBrowsers(); + } + } + + updateBrowser = (key, evt) => { + console.log(key, evt); + const { bookmark, collection, list } = this.props; + this.props.editBk(collection.get('owner'), collection.get('id'), list.get('id'), bookmark.get('id'), {browser: key}); + } + + render() { + const { browsers, bookmark } = this.props; + + const availBrowsers = []; + filterBrowsers.forEach((id) => { + const browser = browsers.get(id); + if (browser) { + availBrowsers.push(browser); + } + }); + + const bkBrowser = browsers ? browsers.find(b => b.get('id') === bookmark.get('browser')) : null; + + return ( + + + { + bkBrowser ? + + {`${bkBrowser.get('name')} + {` ${bkBrowser.get('name')} v${bkBrowser.get('version')}`} + : + 'Current Browser' + } + + + { + availBrowsers.map((b) => { + return ( + + {`${b.get('name')} + {` ${b.get('name')} v${b.get('version')}`} + + ); + }) + } + + + Current browser + + + + ); + } +} + +export default InlineBrowserSelectUI; diff --git a/frontend/src/components/controls/ModeSelectorUI/index.js b/frontend/src/components/controls/ModeSelectorUI/index.js index 00b16d63b..1eca6e1b0 100644 --- a/frontend/src/components/controls/ModeSelectorUI/index.js +++ b/frontend/src/components/controls/ModeSelectorUI/index.js @@ -40,7 +40,7 @@ class ModeSelectorUI extends PureComponent { //window.location.href = `/${user}/${coll}/index`; this.props.history.push(`/${user}/${coll}/manage`); } else { - this.props.history.push(`/${user}/${coll}/manage?search=&session=${rec}`); + this.props.history.push(`/${user}/${coll}/manage?search=&session=${rec}&method=url`); } } @@ -173,7 +173,7 @@ class ModeSelectorUI extends PureComponent { { modeMarkup } { isWrite && } - +
    { isLive && diff --git a/frontend/src/components/controls/RemoteBrowserSelectUI/index.js b/frontend/src/components/controls/RemoteBrowserSelectUI/index.js index d240e868b..c5a65baad 100644 --- a/frontend/src/components/controls/RemoteBrowserSelectUI/index.js +++ b/frontend/src/components/controls/RemoteBrowserSelectUI/index.js @@ -128,7 +128,7 @@ class RemoteBrowserSelectUI extends PureComponent {
    loading options..
    } { loaded && showBrowsers && - showBrowsers.map(browser => ) + showBrowsers.map(browser => ) } { diff --git a/frontend/src/components/controls/RemoteBrowserUI/index.js b/frontend/src/components/controls/RemoteBrowserUI/index.js index 82871b321..b055cab22 100644 --- a/frontend/src/components/controls/RemoteBrowserUI/index.js +++ b/frontend/src/components/controls/RemoteBrowserUI/index.js @@ -302,7 +302,7 @@ class RemoteBrowserUI extends Component { Browser Time Left: {countdown} } -
    + {/*
    */}
    diff --git a/frontend/src/components/controls/RemoteBrowserUI/style.scss b/frontend/src/components/controls/RemoteBrowserUI/style.scss index da676b01f..17d0d3bac 100644 --- a/frontend/src/components/controls/RemoteBrowserUI/style.scss +++ b/frontend/src/components/controls/RemoteBrowserUI/style.scss @@ -1,7 +1,10 @@ @import 'src/vars'; -#browserMsg, #message { - margin-top: 20px; +#browserMsg { + position: absolute; + top: 40px; + left: 0px; + right: 0px; font-size: 18px; text-align: center; } @@ -50,6 +53,7 @@ #browser { width: 100%; + position: relative; } // #noVNC_screen > .canvas { diff --git a/frontend/src/components/controls/ReplayUI/style.scss b/frontend/src/components/controls/ReplayUI/style.scss index 68518c98e..690efa068 100644 --- a/frontend/src/components/controls/ReplayUI/style.scss +++ b/frontend/src/components/controls/ReplayUI/style.scss @@ -101,11 +101,17 @@ } &.embed { + height: 100vh; flex-direction: column; iframe { width: 100%; } + + #browser { + flex-grow: 1; + height: 100%; + } } iframe { diff --git a/frontend/src/components/controls/index.js b/frontend/src/components/controls/index.js index 4f7057bbc..4b47002f6 100644 --- a/frontend/src/components/controls/index.js +++ b/frontend/src/components/controls/index.js @@ -1,8 +1,9 @@ +export AutopilotUI from './AutopilotUI'; export BugReportUI from './BugReportUI'; export ExtractWidgetUI from './ExtractWidgetUI'; export IFrame from './IFrame'; export InfoWidgetUI from './InfoWidgetUI'; -export AutopilotUI from './AutopilotUI'; +export InlineBrowserSelectUI from './InlineBrowserSelectUI'; export ModeSelectorUI from './ModeSelectorUI'; export NewRecordingUI from './NewRecordingUI'; export PatchWidgetUI from './PatchWidgetUI'; diff --git a/frontend/src/components/siteComponents/AdminHeaderUI/style.scss b/frontend/src/components/siteComponents/AdminHeaderUI/style.scss index df2e3e38a..af5085c6e 100644 --- a/frontend/src/components/siteComponents/AdminHeaderUI/style.scss +++ b/frontend/src/components/siteComponents/AdminHeaderUI/style.scss @@ -80,6 +80,7 @@ } .dropdown-item { + white-space: normal; &:not(:hover):not(.active) { color: $white; diff --git a/frontend/src/components/siteComponents/HomeUI/index.js b/frontend/src/components/siteComponents/HomeUI/index.js index 5aafaaad8..eb5f26808 100644 --- a/frontend/src/components/siteComponents/HomeUI/index.js +++ b/frontend/src/components/siteComponents/HomeUI/index.js @@ -183,25 +183,6 @@ class HomeUI extends PureComponent {

    {product} is a project of Rhizome, a registered 501(c)(3) non-profit organization. Your donations are tax-deductible.

    } - -
    -

    Desktop Tools

    -

    In partnership with the Webrecorder project, we aim to make web archiving accessible to all, and ensure interoperability in between tools. Here are some apps we have developed together:

    -
    - -
    - Desktop Logo -

    Webrecorder Desktop App

    -

    Create, manage and store web archives on your local computer; import them to Conifer for public presentation.

    - -
    - -
    - Player Logo -

    Webrecorder Player App

    -

    Export Conifer collections and access them offline.

    - -
    diff --git a/frontend/src/components/siteComponents/NewPasswordUI/index.js b/frontend/src/components/siteComponents/NewPasswordUI/index.js index ce6f13fb1..a8c869199 100644 --- a/frontend/src/components/siteComponents/NewPasswordUI/index.js +++ b/frontend/src/components/siteComponents/NewPasswordUI/index.js @@ -13,7 +13,7 @@ import './style.scss'; class NewPasswordUI extends Component { static propTypes = { - errors: PropTypes.object, + error: PropTypes.object, location: PropTypes.object, match: PropTypes.object, setPassword: PropTypes.func, @@ -57,18 +57,18 @@ class NewPasswordUI extends Component { } render() { - const { errors, location: { search }, success } = this.props; + const { error, location: { search }, success } = this.props; const { newPass, newPass2 } = this.state; const qs = querystring.parse(search.replace('?', '')); return ( { - (success || errors) && - + (success || error) && + { - errors ? - {passwordResetErr[errors.get('error')]} : + error ? + {passwordResetErr[error]} : Your password has been successfully reset! } diff --git a/frontend/src/components/siteComponents/PasswordResetUI/index.js b/frontend/src/components/siteComponents/PasswordResetUI/index.js index af16ebc3c..5ed19ccbf 100644 --- a/frontend/src/components/siteComponents/PasswordResetUI/index.js +++ b/frontend/src/components/siteComponents/PasswordResetUI/index.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { passwordReset as passwordResetErrors } from 'helpers/userMessaging'; import { Alert, Button, Col, Form, Row } from 'react-bootstrap'; import './style.scss'; @@ -9,7 +10,7 @@ import './style.scss'; class ResetPasswordUI extends Component { static propTypes = { cb: PropTypes.func, - errors: PropTypes.object, + error: PropTypes.object, success: PropTypes.bool }; @@ -18,7 +19,6 @@ class ResetPasswordUI extends Component { this.state = { email: '', - error: false, username: '' }; } @@ -29,8 +29,6 @@ class ResetPasswordUI extends Component { if (email || username) { this.props.cb(this.state); - } else { - this.setState({ error: true }); } } @@ -39,19 +37,19 @@ class ResetPasswordUI extends Component { } render() { - const { errors, success } = this.props; + const { error, success } = this.props; const { username, email } = this.state; return ( { - (success || errors) && - + (success || error) && + { - errors ? - Username or email address not found. : - A password reset e-mail has been sent to your e-mail! + error ? + {passwordResetErrors[error] || 'Error encountered'} : + A password reset email will been sent to the email address associated with that account if it exists. } } diff --git a/frontend/src/components/siteComponents/UserManagementUI/index.js b/frontend/src/components/siteComponents/UserManagementUI/index.js index 4d8f714f5..03722d195 100644 --- a/frontend/src/components/siteComponents/UserManagementUI/index.js +++ b/frontend/src/components/siteComponents/UserManagementUI/index.js @@ -176,7 +176,7 @@ class UserManagementUI extends PureComponent {
  • :
  • - + { !__DESKTOP__ &&
  • diff --git a/frontend/src/components/siteComponents/UserSettingsUI/index.js b/frontend/src/components/siteComponents/UserSettingsUI/index.js index 68d57260a..a942d07be 100644 --- a/frontend/src/components/siteComponents/UserSettingsUI/index.js +++ b/frontend/src/components/siteComponents/UserSettingsUI/index.js @@ -29,8 +29,9 @@ import './style.scss'; class UserSettingsUI extends Component { static propTypes = { + adminUpdateUser: PropTypes.func, auth: PropTypes.object, - collSum: PropTypes.number, + collections: PropTypes.object, deleting: PropTypes.bool, deleteError: PropTypes.oneOfType([ PropTypes.string, @@ -40,10 +41,11 @@ class UserSettingsUI extends Component { edited: PropTypes.bool, editing: PropTypes.bool, editUser: PropTypes.func, + indexCollection: PropTypes.func, loadUserRoles: PropTypes.func, match: PropTypes.object, + setCollectionPrivate: PropTypes.func, updatePass: PropTypes.func, - adminUpdateUser: PropTypes.func, user: PropTypes.object }; @@ -76,8 +78,12 @@ class UserSettingsUI extends Component { desc: props.user.get('desc'), display_url: props.user.get('display_url'), full_name: props.user.get('full_name'), + indexColl: null, + newEmail: '', password: '', password2: '', + privateColl: null, + reIndexColl: false, role: null, showModal: false }; @@ -90,7 +96,19 @@ class UserSettingsUI extends Component { } } - handleChange = evt => this.setState({ [evt.target.name]: evt.target.value }) + handleChange = (evt) => { + if (evt.target.type === 'checkbox') { + if (evt.target.name in this.state) { + this.setState({ [evt.target.name]: !this.state[evt.target.name] }); + } else { + this.setState({ [evt.target.name]: true }); + } + } else { + this.setState({ + [evt.target.name]: evt.target.value + }); + } + } goToSupporterPortal = () => { window.location.href = supporterPortal; @@ -104,6 +122,18 @@ class UserSettingsUI extends Component { this.setState({ desc }); } + triggerIndexColl = () => { + const { user } = this.props; + const { indexColl, reIndexColl } = this.state; + + if (indexColl) { + this.props.indexCollection(user.get('username'), indexColl, reIndexColl); + + // reset widget + this.setState({ indexColl: null, reIndexColl: false }); + } + } + setRole = (role) => { this.setState({ role }); } @@ -127,12 +157,42 @@ class UserSettingsUI extends Component { } } + selectIndexColl = (coll) => { + this.setState({indexColl: coll}); + } + + selectPrivateColl = (coll) => { + this.setState({privateColl: coll}); + } + sendDelete = (evt) => { if (this.validateConfirmDelete()) { this.props.deleteUser(this.props.auth.getIn(['user', 'username'])); } } + setPrivate = () => { + const { privateColl } = this.state; + const { match: { params: { user } } } = this.props; + + this.props.setCollectionPrivate(user, privateColl); + this.setState({privateColl: null}); + } + + suspendAccount = () => { + const { match: { params: { user } }, adminUpdateUser } = this.props; + adminUpdateUser(user, { role: 'suspended' }); + } + + updateEmail = () => { + const { match: { params: { user } }, adminUpdateUser } = this.props; + const { newEmail } = this.state; + + if (this.validateEmail()) { + adminUpdateUser(user, { email_addr: newEmail }); + } + } + updateUserAllotment = () => { const { match: { params: { user } }, adminUpdateUser } = this.props; const { allotment } = this.state; @@ -180,6 +240,16 @@ class UserSettingsUI extends Component { return true; } + validateEmail = () => { + const { newEmail } = this.state; + + if (!newEmail || newEmail.indexOf('@') === -1 || newEmail.match(/\.\w+$/) === null) { + return false; + } + + return true; + } + validatePassword = () => { const { password, password2, missingPw } = this.state; @@ -195,8 +265,8 @@ class UserSettingsUI extends Component { } render() { - const { auth, deleting, edited, editing, match: { params }, user } = this.props; - const { currPassword, password, password2, showModal } = this.state; + const { auth, collections, deleting, edited, editing, match: { params }, user } = this.props; + const { currPassword, indexColl, password, password2, privateColl, showModal } = this.state; const username = params.user; const canAdmin = username === auth.getIn(['user', 'username']); @@ -210,6 +280,8 @@ class UserSettingsUI extends Component { const totalSpace = user.getIn(['space_utilization', 'total']); const passUpdate = auth.get('passUpdate'); const passUpdateFail = auth.get('passUpdateFail'); + const selectedCollForIndex = indexColl ? collections.find(c => c.get('id') === indexColl) : null; + const selectedCollForPrivate = privateColl ? collections.find(c => c.get('id') === privateColl) : null; const confirmDeleteBody = (
    @@ -286,7 +358,7 @@ class UserSettingsUI extends Component {
    Update Role
    -

    Current Role: {user.get('role')}

    +

    Current Role: {user.get('role')}

    {this.state.role ? this.state.role : 'Change Role'} @@ -300,10 +372,57 @@ class UserSettingsUI extends Component {
    +
    +
    Index Collection
    +

    Select one of the user's collection below to trigger indexing.

    + + {indexColl ? selectedCollForIndex.get('title') : 'Select a collection to index'} + + { + collections.map(c => {c.get('title')}) + } + + +
    + + +
    + +
    +
    Set Collection Private
    +

    Select one of the user's public collections below to mark private

    + + {privateColl ? selectedCollForPrivate.get('title') : 'Public Collections'} + + { + collections.map(c => c.get('public') && {c.get('title')}) + } + + + +
    +
    Suspend Account
    -

    User will be suspended and a notification will be sent via email.

    - +

    User will be suspended and receive notice on attempted login.

    + +
    + +
    +
    Update Email
    + + + 6 && !this.validateEmail()} + value={this.state.newEmail} /> + + +
    diff --git a/frontend/src/components/siteComponents/UserSettingsUI/style.scss b/frontend/src/components/siteComponents/UserSettingsUI/style.scss index bb64f4810..d45f749e9 100644 --- a/frontend/src/components/siteComponents/UserSettingsUI/style.scss +++ b/frontend/src/components/siteComponents/UserSettingsUI/style.scss @@ -52,6 +52,15 @@ h5 { margin-bottom: rem(1rem); } + + mark.role-suspended { + background: $warning; + color: white; + } + } + + .dropdown .dropdown-item { + white-space: break-spaces; } } } diff --git a/frontend/src/components/siteComponents/UserSignupUI/index.js b/frontend/src/components/siteComponents/UserSignupUI/index.js index ca61c6f65..7ca5c1867 100644 --- a/frontend/src/components/siteComponents/UserSignupUI/index.js +++ b/frontend/src/components/siteComponents/UserSignupUI/index.js @@ -1,10 +1,11 @@ import React, { Component } from 'react'; import classNames from 'classnames'; -import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; +import ReCAPTCHA from "react-google-recaptcha"; +import { Link } from 'react-router-dom'; import { Alert, Button, Col, Form, Row } from 'react-bootstrap'; -import { product, userRegex } from 'config'; +import { product, recaptcha, recaptchaKey, userRegex } from 'config'; import { registration as registrationErr } from 'helpers/userMessaging'; import { passwordPassRegex } from 'helpers/utils'; @@ -32,6 +33,8 @@ class UserSignup extends Component { constructor(props) { super(props); + this.recaptchaRef = React.createRef(); + this.state = { moveTemp: true, toColl: 'New Collection', @@ -44,8 +47,12 @@ class UserSignup extends Component { }; } - save = (evt) => { + save = async (evt) => { evt.preventDefault(); + + // invoke recaptcha + const captchaToken = recaptcha ? await this.recaptchaRef.current.executeAsync() : null; + const { user } = this.props; const { announce_mailer, @@ -64,7 +71,7 @@ class UserSignup extends Component { this.validateEmail() ) { // core fields to send to server - let data = { username, email, password, confirmpassword }; + let data = { captchaToken, username, email, password, confirmpassword }; if (announce_mailer) { data = { ...data, announce_mailer }; @@ -325,6 +332,10 @@ class UserSignup extends Component { toColl={toColl} /> } + { + recaptcha && + } +