Skip to content

Commit

Permalink
public: Add link editing view
Browse files Browse the repository at this point in the history
This enables the user to update the target URL of an existing link, or
to assign the link to a new owner. All of the major functionality of the
application is now complete.
  • Loading branch information
mbland committed Aug 8, 2017
1 parent 7326f4c commit bd0978e
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 50 deletions.
56 changes: 47 additions & 9 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@
link.relative + ' has been deleted', link.relative + ' wasn\'t deleted')
}

cl.Backend.prototype.updateTarget = function(link, target) {
link = cl.createLinkInfo(link)
return this.makeApiCall('POST', 'target', link, { target: target },
link.anchor + ' now redirects to ' + target,
'The target URL wasn\'t updated')
}

cl.Backend.prototype.changeOwner = function(link, owner) {
link = cl.createLinkInfo(link)
return this.makeApiCall('POST', 'owner', link, { owner: owner },
owner + ' now owns ' + link.anchor, 'Ownership wasn\'t transferred')
}

cl.backend = new cl.Backend(cl.xhr)

cl.loadApp = function() {
Expand Down Expand Up @@ -252,9 +265,8 @@

cl.completeEditLinkView = function(origData, link) {
var view = cl.getTemplate('edit-view'),
forms = view.getElementsByTagName('form'),
buttons = view.getElementsByTagName('button'),
targetButton = buttons[0],
ownerButton = buttons[1],
data = {
link: link.relative,
target: origData.target,
Expand All @@ -265,21 +277,47 @@
}

cl.applyData(data, view)
targetButton.onclick = cl.updateTargetClick
ownerButton.onclick = cl.changeOwnerClick

buttons[0].onclick = cl.createClickHandler(forms[0], 'updateTarget',
{ link: link.trimmed, original: data.target })
buttons[1].onclick = function(e) {
e.preventDefault()
cl.changeOwner(forms[1], link, origData.owner)
}
return Promise.resolve(new cl.View(view, function() {
cl.focusFirstElement(view, 'input')
document.activeElement.setSelectionRange(0, data.target.length)
}))
}

cl.updateTargetClick = function(e) {
e.preventDefault()
cl.updateTarget = function(view, data) {
var target = view.querySelector('[data-name=target]').value

if (target === data.original) {
return Promise.resolve('The target URL remains the same.')
}
return cl.validateTarget(target) ||
cl.backend.updateTarget(data.link, target)
}

cl.changeOwner = function(form, link, origOwner) {
var owner = form.querySelector('[data-name=owner]').value,
result = form.getElementsByClassName('result')[0],
noChange

if (owner === origOwner) {
noChange = cl.getTemplate('result success')
noChange.innerHTML = 'The owner remains the same.'
return cl.flashElement(result, noChange.outerHTML)
}
cl.confirmTransfer(link, owner, result).open()
}

cl.changeOwnerClick = function(e) {
e.preventDefault()
cl.confirmTransfer = function(link, newOwner, resultElement) {
var data = { link: link.relative, owner: newOwner }

return new cl.Dialog('confirm-transfer', data, function() {
return cl.backend.changeOwner(link.trimmed, newOwner)
}, resultElement)
}

cl.linksView = function() {
Expand Down
12 changes: 11 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
.clicks-action{text-align:right;}
.clicks-action button{width:100%;}
.dialog-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,0.3);z-index:2;}
.dialog {width:375px;position:relative;background:whitesmoke;border-radius:5px;margin:20% auto;padding:10px 0;text-align:center;font-weight:bold;border:medium solid black;z-index:3;}
.dialog {width:350px;position:relative;background:whitesmoke;border-radius:5px;margin:20% auto;padding:10px;text-align:center;font-weight:bold;border:medium solid black;z-index:3;}
.edit-view .info{margin-bottom:1em;}
.edit-view .info .row{display:flex;justify-content:flex-start;}
.edit-view .label{font-weight:bold;width:110px;}
Expand Down Expand Up @@ -146,6 +146,16 @@ <h3 class='title'>Confirm delete</h3>
<button class='confirm'>Yes</button>
<button class='focused cancel button-primary'>No</button>
</div>
<div class='confirm-transfer dialog'>
<h3 class='title'>Confirm ownership transfer</h3>
<p class='description'>
Are you sure you wish to transfer ownership of
<span data-name='link'>[link]</span> to
<span data-name='owner'>[owner]</span>?
</p>
<button class='confirm'>Yes</button>
<button class='focused cancel button-primary'>No</button>
</div>
</div>
</div>
<script>
Expand Down
2 changes: 0 additions & 2 deletions public/tests/redirect-target.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
<head>
<title>End-to-End test redirect target page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="vendor/mocha.css" />
</head>
<body>
<p>This is the redirect target page for end-to-end tests.</p>
Expand Down
203 changes: 167 additions & 36 deletions public/tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,32 @@ describe('Custom Links', function() {
undefined, 'Failed to get link info for /foo')
})
})

describe('updateTarget', function() {
it('calls /api/update', function() {
stubOut(backend, 'makeApiCall')
backend.updateTarget('foo', LINK_TARGET)
backend.makeApiCall.calledOnce.should.be.true
checkMakeApiCallArgs('POST', 'target',
cl.createLinkInfo('foo'), { target: LINK_TARGET },
'<a href=\'/foo\'>' + window.location.origin +
'/foo</a> now redirects to ' + LINK_TARGET,
'The target URL wasn\'t updated')
})
})

describe('changeOwner', function() {
it('calls /api/owner', function() {
stubOut(backend, 'makeApiCall')
backend.changeOwner('foo', USER_ID)
backend.makeApiCall.calledOnce.should.be.true
checkMakeApiCallArgs('POST', 'owner',
cl.createLinkInfo('foo'), { owner: USER_ID },
USER_ID + ' now owns ' + '<a href=\'/foo\'>' +
window.location.origin + '/foo</a>',
'Ownership wasn\'t transferred')
})
})
})

describe('loadApp', function() {
Expand Down Expand Up @@ -1428,29 +1454,109 @@ describe('Custom Links', function() {
})
})

describe('updateTargetClick', function() {
var event
describe('updateTarget', function() {
var targetForm, targetField, data, expectBackendCall

beforeEach(function() {
event = { preventDefault: sinon.spy() }
stubOut(cl.backend, 'updateTarget')
targetForm = cl.getTemplate('edit-view').getElementsByTagName('form')[0]
targetField = targetForm.querySelector('[data-name=target]')
data = {
link: 'foo',
original: LINK_TARGET
}
})

it('should prevent the default event action', function() {
cl.updateTargetClick(event)
event.preventDefault.called.should.be.true
expectBackendCall = function() {
return cl.backend.updateTarget.withArgs('foo', LINK_TARGET + 'foo')
}

it('doesn\'t send a request if the target URL hasn\'t changed', function() {
targetField.value = LINK_TARGET
return cl.updateTarget(targetForm, data).then(function(result) {
result.should.equal('The target URL remains the same.')
cl.backend.updateTarget.called.should.be.false
})
})

it('fails if the target URL doesn\'t pass validation', function() {
targetField.value = 'gopher://foo.com'
return cl.updateTarget(targetForm, data)
.should.be.rejectedWith(
'Target URL protocol must be http:// or https://.')
.then(function() {
cl.backend.updateTarget.called.should.be.false
})
})

it('successfully updates the target URL', function() {
targetField.value = LINK_TARGET + 'foo'
expectBackendCall().returns(Promise.resolve('success'))
return cl.updateTarget(targetForm, data).should.become('success')
})
})

describe('changeOwnerClick', function() {
var event
describe('changeOwner', function() {
var ownerForm, ownerField, link

beforeEach(function() {
event = { preventDefault: sinon.spy() }
stubOut(cl, 'confirmTransfer')
ownerForm = prepareFlashingElement(
cl.getTemplate('edit-view').getElementsByTagName('form')[1])
ownerField = ownerForm.querySelector('[data-name=owner]')
link = cl.createLinkInfo('foo')
})

it('should prevent the default event action', function() {
cl.changeOwnerClick(event)
event.preventDefault.called.should.be.true
afterEach(function() {
clTest.removeElement(ownerForm)
})

it('doesn\'t send a request if the owner hasn\'t changed', function() {
ownerField.value = USER_ID
return cl.changeOwner(ownerForm, link, USER_ID).then(function(element) {
element.textContent.should.equal('The owner remains the same.')
cl.confirmTransfer.called.should.be.false
})
})

it('opens a dialog box to confirm the ownership transfer', function() {
var result = ownerForm.getElementsByClassName('result')[0],
openSpy = sinon.spy()

ownerField.value = 'foo@bar.com'
cl.confirmTransfer.withArgs(link, ownerField.value, result)
.returns({ open: openSpy })

cl.changeOwner(ownerForm, link, USER_ID)
openSpy.called.should.be.true
})
})

describe('confirmTransfer', function() {
var dialog, link, result

beforeEach(function() {
stubOut(cl.backend, 'changeOwner')
link = cl.createLinkInfo('foo')
result = prepareFlashingElement(document.createElement('div'))
dialog = cl.confirmTransfer(link, USER_ID, result)
dialog.open()
})

afterEach(function() {
dialog.close()
clTest.removeElement(result)
})

it('opens a dialog box to confirm the ownership transfer', function() {
dialog.element.parentNode.should.equal(document.body)
cl.backend.changeOwner.withArgs(link.trimmed, USER_ID)
.returns(Promise.resolve('transferred'))
dialog.confirm.click()
return dialog.operation.then(function() {
cl.backend.changeOwner.calledOnce.should.be.true
result.textContent.should.equal('transferred')
})
})
})

Expand Down Expand Up @@ -1493,36 +1599,61 @@ describe('Custom Links', function() {
.to.equal(cl.dateStamp(data.updated))
})

it('prepares the target update form', function() {
var updateTarget = view.element.getElementsByTagName('form')[0],
targetField,
submitButton
describe('the target URL form', function() {
var updateTarget, targetField, submitButton, resultElement

beforeEach(function() {
updateTarget = view.element.getElementsByTagName('form')[0]
expect(updateTarget).to.not.be.undefined
targetField = updateTarget.getElementsByTagName('input')[0],
expect(targetField).to.not.be.undefined
submitButton = updateTarget.getElementsByTagName('button')[0]
expect(submitButton).to.not.be.undefined
resultElement = updateTarget.getElementsByClassName('result')[0]
expect(resultElement).to.not.be.undefined
})

it('sets the active element to the target field', function() {
document.activeElement.should.equal(targetField)
})

it('sets the target URL value to the existing target', function() {
document.activeElement.value.should.equal(data.target)
})

expect(updateTarget).to.not.be.undefined
targetField = updateTarget.getElementsByTagName('input')[0],
expect(targetField).to.not.be.undefined
submitButton = updateTarget.getElementsByTagName('button')[0]
expect(submitButton).to.not.be.undefined
it('selects the entire target URL value', function() {
clTest.getSelection().should.equal(data.target)
})

document.activeElement.should.equal(targetField)
document.activeElement.value.should.equal(data.target)
clTest.getSelection().should.equal(data.target)
submitButton.onclick.should.equal(cl.updateTargetClick)
it('assigns a submit handler to call cl.updateTarget', function() {
submitButton.onclick().should.eql([updateTarget, 'updateTarget',
{ link: link.trimmed, original: data.target }])
})
})

it('prepares the change owner form', function() {
var changeOwner = view.element.getElementsByTagName('form')[1],
ownerField,
submitButton
describe('the change owner form', function() {
var changeOwner, ownerField, submitButton, resultElement

expect(changeOwner).to.not.be.undefined
ownerField = changeOwner.getElementsByTagName('input')[0],
expect(ownerField).to.not.be.undefined
submitButton = changeOwner.getElementsByTagName('button')[0]
expect(submitButton).to.not.be.undefined
beforeEach(function() {
changeOwner = view.element.getElementsByTagName('form')[1],
expect(changeOwner).to.not.be.undefined
ownerField = changeOwner.getElementsByTagName('input')[0],
ownerField.value.should.equal(data.owner)
expect(ownerField).to.not.be.undefined
submitButton = changeOwner.getElementsByTagName('button')[0]
expect(submitButton).to.not.be.undefined
resultElement = changeOwner.getElementsByClassName('result')[0]
expect(resultElement).to.not.be.undefined
})

ownerField.value.should.equal(data.owner)
submitButton.onclick.should.equal(cl.changeOwnerClick)
it('assigns a submit handler to call cl.changeOwner', function() {
var event = { preventDefault: sinon.spy() }

stubOut(cl, 'changeOwner')
submitButton.onclick(event)
event.preventDefault.calledOnce.should.be.true
cl.changeOwner.calledWith(changeOwner, link, data.owner).should.be.true
})
})
})
})
10 changes: 10 additions & 0 deletions public/tests/updated-target.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>End-to-End test updated target page</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<p>This is the updated target page for end-to-end tests.</p>
</body>
</html>
Loading

0 comments on commit bd0978e

Please sign in to comment.