kopia lustrzana https://github.com/OpenDroneMap/WebODM
Added clipboardinput, sharing logic, CSS fixes
rodzic
2552b82e4f
commit
39c391451f
|
@ -189,7 +189,7 @@ pre.prettyprint,
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Success */
|
/* Success */
|
||||||
.task-list-item .status-label.done{
|
.task-list-item .status-label.done, .theme-background-success{
|
||||||
background-color: theme("success");
|
background-color: theme("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,16 @@ export default {
|
||||||
|
|
||||||
clone: function(obj){
|
clone: function(obj){
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
},
|
||||||
|
|
||||||
|
// "/a/b" --> http://localhost/a/b
|
||||||
|
absoluteUrl: function(path, href = window.location.href){
|
||||||
|
if (path[0] === '/') path = path.slice(1);
|
||||||
|
|
||||||
|
let parser = document.createElement('a');
|
||||||
|
parser.href = window.location.href;
|
||||||
|
|
||||||
|
return `${parser.protocol}//${parser.host}/${path}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Clipboard from 'clipboard';
|
||||||
|
import '../css/ClipboardInput.scss';
|
||||||
|
|
||||||
|
class ClipboardInput extends React.Component{
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showCopied: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
this.clipboard = new Clipboard(this.dom, {
|
||||||
|
target: () => this.dom
|
||||||
|
}).on('success', () => {
|
||||||
|
this.setState({showCopied: true});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(){
|
||||||
|
this.clipboard.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div className="clipboardInput">
|
||||||
|
<input
|
||||||
|
{...this.props}
|
||||||
|
ref={(domNode) => { this.dom = domNode; }}
|
||||||
|
onBlur={() => { this.setState({showCopied: false}); }}
|
||||||
|
/>
|
||||||
|
<div style={{position: 'relative', 'width': '100%'}}>
|
||||||
|
<div className={"copied theme-background-success " + (this.state.showCopied ? "show" : "")}>Copied to clipboard!</div>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ClipboardInput;
|
|
@ -51,6 +51,7 @@ class Map extends React.Component {
|
||||||
|
|
||||||
this.loadImageryLayers = this.loadImageryLayers.bind(this);
|
this.loadImageryLayers = this.loadImageryLayers.bind(this);
|
||||||
this.updatePopupFor = this.updatePopupFor.bind(this);
|
this.updatePopupFor = this.updatePopupFor.bind(this);
|
||||||
|
this.handleMapMouseDown = this.handleMapMouseDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePopupFor(layer){
|
updatePopupFor(layer){
|
||||||
|
@ -257,16 +258,29 @@ class Map extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMapMouseDown(e){
|
||||||
|
// Make sure the share popup closes
|
||||||
|
this.shareButton.hidePopup();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div style={{height: "100%"}} className="map">
|
<div style={{height: "100%"}} className="map">
|
||||||
<ErrorMessage bind={[this, 'error']} />
|
<ErrorMessage bind={[this, 'error']} />
|
||||||
<div
|
<div
|
||||||
style={{height: "100%"}}
|
style={{height: "100%"}}
|
||||||
ref={(domNode) => (this.container = domNode)}>
|
ref={(domNode) => (this.container = domNode)}
|
||||||
|
onMouseDown={this.handleMapMouseDown}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="actionButtons">
|
||||||
{this.state.singleTask !== null ?
|
{this.state.singleTask !== null ?
|
||||||
<ShareButton task={this.state.singleTask} />
|
<ShareButton
|
||||||
|
ref={(ref) => { this.shareButton = ref; }}
|
||||||
|
task={this.state.singleTask}
|
||||||
|
/>
|
||||||
: ""}
|
: ""}
|
||||||
<SwitchModeButton
|
<SwitchModeButton
|
||||||
task={this.state.singleTask}
|
task={this.state.singleTask}
|
||||||
|
|
|
@ -15,26 +15,41 @@ class ShareButton extends React.Component {
|
||||||
constructor(props){
|
constructor(props){
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { showPopup: false };
|
this.state = {
|
||||||
|
showPopup: false,
|
||||||
|
task: props.task
|
||||||
|
};
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.handleTaskChanged = this.handleTaskChanged.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(){
|
handleClick(){
|
||||||
this.setState({ showPopup: !this.state.showPopup });
|
this.setState({ showPopup: !this.state.showPopup });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hidePopup(){
|
||||||
|
this.setState({showPopup: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTaskChanged(task){
|
||||||
|
this.setState({task});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="shareButton">
|
<div className="shareButton" onClick={e => { e.stopPropagation(); }}>
|
||||||
{this.state.showPopup ?
|
{this.state.showPopup ?
|
||||||
<SharePopup task={this.props.task} />
|
<SharePopup
|
||||||
|
task={this.state.task}
|
||||||
|
taskChanged={this.handleTaskChanged}
|
||||||
|
/>
|
||||||
: ""}
|
: ""}
|
||||||
<button
|
<button
|
||||||
ref={(domNode) => { this.shareButton = domNode; }}
|
ref={(domNode) => { this.shareButton = domNode; }}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
className={"shareButton btn btn-sm " + (this.props.task.public ? "btn-primary" : "btn-secondary")}>
|
className={"shareButton btn btn-sm " + (this.state.task.public ? "btn-primary" : "btn-secondary")}>
|
||||||
<i className="fa fa-share-alt"></i> Share
|
<i className="fa fa-share-alt"></i> Share
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,12 +2,16 @@ import React from 'react';
|
||||||
import '../css/SharePopup.scss';
|
import '../css/SharePopup.scss';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ErrorMessage from './ErrorMessage';
|
import ErrorMessage from './ErrorMessage';
|
||||||
|
import Utils from '../classes/Utils';
|
||||||
|
import ClipboardInput from './ClipboardInput';
|
||||||
|
|
||||||
class SharePopup extends React.Component{
|
class SharePopup extends React.Component{
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
task: PropTypes.object.isRequired
|
task: PropTypes.object.isRequired,
|
||||||
|
taskChanged: PropTypes.func
|
||||||
};
|
};
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
taskChanged: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -22,6 +26,12 @@ class SharePopup extends React.Component{
|
||||||
this.handleEnableSharing = this.handleEnableSharing.bind(this);
|
this.handleEnableSharing = this.handleEnableSharing.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(){
|
||||||
|
if (!this.state.task.public){
|
||||||
|
this.handleEnableSharing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleEnableSharing(e){
|
handleEnableSharing(e){
|
||||||
const { task } = this.state;
|
const { task } = this.state;
|
||||||
|
|
||||||
|
@ -31,13 +41,14 @@ class SharePopup extends React.Component{
|
||||||
url: `/api/projects/${task.project}/tasks/${task.id}/`,
|
url: `/api/projects/${task.project}/tasks/${task.id}/`,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({
|
data: JSON.stringify({
|
||||||
public: e.target.checked
|
public: !this.state.task.public
|
||||||
}),
|
}),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
type: 'PATCH'
|
type: 'PATCH'
|
||||||
})
|
})
|
||||||
.done((task) => {
|
.done((task) => {
|
||||||
this.setState({task});
|
this.setState({task});
|
||||||
|
this.props.taskChanged(task);
|
||||||
})
|
})
|
||||||
.fail(() => this.setState({error: "An error occurred. Check your connection and permissions."}))
|
.fail(() => this.setState({error: "An error occurred. Check your connection and permissions."}))
|
||||||
.always(() => {
|
.always(() => {
|
||||||
|
@ -46,13 +57,17 @@ class SharePopup extends React.Component{
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
|
const shareLink = Utils.absoluteUrl(`/public/task/${this.state.task.id}/map/`);
|
||||||
|
const iframeUrl = Utils.absoluteUrl(`public/task/${this.state.task.id}/iframe/`);
|
||||||
|
const iframeCode = `<iframe>${iframeUrl}</iframe>`;
|
||||||
|
|
||||||
return (<div className="sharePopup popover top in">
|
return (<div className="sharePopup popover top in">
|
||||||
<div className="arrow"></div>
|
<div className="arrow"></div>
|
||||||
<h3 className="popover-title theme-background-highlight">Share</h3>
|
<h3 className="popover-title theme-background-highlight">Share This Task</h3>
|
||||||
<div className="popover-content theme-secondary">
|
<div className="popover-content theme-secondary">
|
||||||
<ErrorMessage bind={[this, 'error']} />
|
<ErrorMessage bind={[this, 'error']} />
|
||||||
<div className="checkbox">
|
<div className="checkbox">
|
||||||
<label>
|
<label onClick={this.handleEnableSharing}>
|
||||||
{this.state.togglingShare ?
|
{this.state.togglingShare ?
|
||||||
<i className="fa fa-refresh fa-spin fa-fw"></i>
|
<i className="fa fa-refresh fa-spin fa-fw"></i>
|
||||||
: ""}
|
: ""}
|
||||||
|
@ -61,15 +76,35 @@ class SharePopup extends React.Component{
|
||||||
className={this.state.togglingShare ? "hide" : ""}
|
className={this.state.togglingShare ? "hide" : ""}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.task.public}
|
checked={this.state.task.public}
|
||||||
onChange={this.handleEnableSharing}
|
onChange={() => {}}
|
||||||
/>
|
/>
|
||||||
Enable sharing
|
Enabled
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={"share-links " + (this.state.task.public ? "show" : "")}>
|
||||||
{this.state.task.public ?
|
<div className="form-group">
|
||||||
<div>A large<br/>bunch<br/>bunch<br/>bunch<br/>bunch<br/>bunch<br/></div>
|
<label>
|
||||||
: ""}
|
Link:
|
||||||
|
<ClipboardInput
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={shareLink}
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>
|
||||||
|
HTML iframe:
|
||||||
|
<ClipboardInput
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={iframeCode}
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import ClipboardInput from '../ClipboardInput';
|
||||||
|
|
||||||
|
describe('<ClipboardInput />', () => {
|
||||||
|
it('renders without exploding', () => {
|
||||||
|
const wrapper = shallow(<ClipboardInput type="text" />);
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
})
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
.clipboardInput{
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.copied{
|
||||||
|
position: absolute;
|
||||||
|
font-size: 70%;
|
||||||
|
left: 50%;
|
||||||
|
width: 110px;
|
||||||
|
margin-left: -55px;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity .2s ease-in-out;
|
||||||
|
|
||||||
|
&.show{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
.shareButton{
|
.shareButton{
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
bottom: 12px;
|
bottom: -11px;
|
||||||
right: 38px;
|
right: 38px;
|
||||||
|
|
||||||
button{
|
button{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.sharePopup{
|
.sharePopup{
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -56px;
|
top: -32px;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
&.popover.top > .arrow{
|
&.popover.top > .arrow{
|
||||||
|
@ -20,5 +20,22 @@
|
||||||
margin-left: -25px;
|
margin-left: -25px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-links{
|
||||||
|
& > div{
|
||||||
|
margin-top: 8px;
|
||||||
|
&:first-child{
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 1s ease-in-out;
|
||||||
|
|
||||||
|
&.show{
|
||||||
|
max-height: 800px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
bottom: 24px;
|
bottom: -22px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"babel-plugin-transform-class-properties": "^6.18.0",
|
"babel-plugin-transform-class-properties": "^6.18.0",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"clipboard": "^1.7.1",
|
||||||
"css-loader": "^0.25.0",
|
"css-loader": "^0.25.0",
|
||||||
"d3": "^3.5.5",
|
"d3": "^3.5.5",
|
||||||
"enzyme": "^2.9.1",
|
"enzyme": "^2.9.1",
|
||||||
|
|
Ładowanie…
Reference in New Issue