Skip to content

Commit

Permalink
[wizard] Read and save redirects.
Browse files Browse the repository at this point in the history
  • Loading branch information
mayakokits authored and torotil committed Jan 11, 2018
1 parent ae57129 commit 7c8f9e4
Show file tree
Hide file tree
Showing 14 changed files with 324 additions and 55 deletions.
2 changes: 1 addition & 1 deletion campaignion_wizard/css/redirects_app/redirects_app.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

64 changes: 64 additions & 0 deletions campaignion_wizard/redirects_app/README.md
Expand Up @@ -3,6 +3,59 @@
## API

### Initial data via Drupal.settings
``` js
Drupal.settings.campaignion_wizard.campaignion_wizard--2 = {
default_redirect_url: 'http://old-default-url.com', // This can be handed over for migration - the app saves everything in the new format. If there is a redirects array with one ore more items, default_redirect_url is ignored.
redirects: [
{
id: 1,
label: 'My internal label',
destination: 'node/20',
prettyDestination: 'Pretty title of my node (20)',
filters: [
{
id: 1,
type: 'opt-in',
value: true
},
{
id: 2,
type: 'submission-field',
field: 'f_name',
operator: 'contains',
value: 'foo'
}
]
},
{
id: 2,
label: '',
destination: 'http://example.com',
prettyDestination: 'http://example.com',
filters: []
}
],
fields: [
// filterable fields in the form submission
{
id: 'f_name',
label: 'First name'
},
{
id: 'l_name',
label: 'Last name'
},
{
id: 'email',
label: 'Email address'
}
],
endpoints: {
nodes: '/getnodes', // GET nodes list
redirects: '/node/8/save-my-redirects' // POST redirects
}
}
```

### Get nodes list
`GET <nodes_endpoint>?q=<search term or nid>`
Expand Down Expand Up @@ -44,11 +97,22 @@ JSON Response:
"value": "foo"
}
]
},
{
"id": null,
"label": "",
"destination": "http://example.com",
"prettyDestination": "http://example.com",
"filters": []
}
]
}
```

The last redirect in the list is the default one. It's not supposed to have either label or filters. Server-side validation only if the user selected the 'custom redirect' radio. If the user changes redirects and afterwards chooses 'Create new thank you page', the app wants to save the changed redirects but does not validate them. They are not used but should be preserved for the future. The server should allow for that.
The `ìd` of everything is `null` when created in the app. The backend gives ids to redirects and filters.
`prettyDestination` has to be saved in the database along with `destination`. If both fields hold the same string, the user entered a custom url. if they differ, `destination` has the format `node/<nid>`.

Operators:

* `==` is
Expand Down
9 changes: 8 additions & 1 deletion campaignion_wizard/redirects_app/build/drupal-fixture.js
@@ -1,9 +1,16 @@
// This module is used to provide the Drupal global in development and test mode.
// Functions taken from drupal.js.

import exampleData from '../test/fixtures/example-data'
import initialData from '../test/fixtures/initial-data'

const Drupal = {
settings: {
// My settings here...
campaignion_wizard: {
'personalized-redirects-widget--5': (process.env.NODE_ENV === 'development')
? exampleData
: initialData
}
},

locale: {},
Expand Down
33 changes: 32 additions & 1 deletion campaignion_wizard/redirects_app/index.html
Expand Up @@ -5,7 +5,38 @@
<title>redirects_app</title>
</head>
<body>
<div class="personalized-redirects-widget"></div>
<a href="#" id="trigger-request-leave-page">Trigger request-leave-page</a>
<a href="#" id="trigger-request-submit-page">Trigger request-submit-page</a>
<div class="personalized-redirects-widget" id="personalized-redirects-widget--5"></div>
<!-- built files will be auto injected -->
<script type="text/javascript">
(function () {
function dispatch (el, type) {
const e = document.createEvent('Event')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
var appHasListeners = false
function addListeners (app) {
if (!appHasListeners) {
app.addEventListener('resume-leave-page', function () {
alert('You can leave the page now.')
})
app.addEventListener('cancel-leave-page', function () {
alert('Just stay here for a moment.')
})
appHasListeners = true
}
}

document.querySelectorAll('[id^=trigger-]').forEach(function (el) {
el.addEventListener('click', function () {
var app = document.querySelector('[data-interrupt-submit]')
addListeners(app)
dispatch(app, el.id.substr(8))
})
})
})()
</script>
</body>
</html>
108 changes: 89 additions & 19 deletions campaignion_wizard/redirects_app/src/App.vue
@@ -1,5 +1,5 @@
<template>
<div class="redirect-app" data-interrupt-submit>
<div class="redirect-app" data-interrupt-submit :data-has-unsaved-changes="unsavedChanges">
<ElButton @click="newRedirect()">{{ text('Add redirect') }}</ElButton>
<RedirectList/>

Expand All @@ -13,8 +13,8 @@
:show-dropdown-on-focus="true"
data-key="values"
label-key="label"
url="http://foo.bar.com"
:headers="{'Authorization': 'JWT foo.bar.3456ß8971230469827456.jklcnfgb'}"
:url="$root.$options.settings.endpoints.nodes"
:headers="{}"
search-param="q"
:count="20"
@input="item => {destination = item}"
Expand All @@ -28,7 +28,9 @@

<script>
import {mapState} from 'vuex'
import {dispatch, validateDestination} from '@/utils'
import {isEqual} from 'lodash'
import {clone, dispatch, validateDestination} from '@/utils'
import api from '@/utils/api'
import RedirectList from './components/RedirectList'
import RedirectDialog from './components/RedirectDialog'
import DestinationField from './components/DestinationField'
Expand Down Expand Up @@ -69,27 +71,80 @@ export default {
destinationIsValid () {
return validateDestination(this.defaultRedirect.destination)
},
unsavedChanges () {
if (this.redirects.length !== this.initialData.redirects.length) return true
if (!isEqual(this.defaultRedirect, this.initialData.defaultRedirect)) return true
for (let i = 0, j = this.redirects.length; i < j; i++) {
if (!isEqual(this.redirects[i], this.initialData.redirects[i])) return true
}
return false
},
...mapState([
'defaultRedirect'
'redirects',
'defaultRedirect',
'initialData'
])
},
created () {
// Shortcut to settings.
this.$root.$options.settings = Drupal.settings.campaignion_wizard[this.$root.$options.drupalContainer.id]
},
mounted () {
// Initialize data
this.$store.commit({
type: 'initData',
redirects: this.$root.$options.settings.redirects,
defaultRedirectUrl: this.$root.$options.settings.default_redirect_url
})
// Handle events from interrupt-submit.js
const listener = e => {
const leavePage = () => {
dispatch(this.$root.$el, 'resume-leave-page')
}
const stayOnPage = () => {
dispatch(this.$root.$el, 'cancel-leave-page')
}
if (e.type === 'request-leave-page') {
// TODO User wants to go back - ask: lose data?
// User clicked 'back' button.
// Forget about unsaved changes if the app is hidden.
// TODO if (appIsHidden) {leavePage(); return}
if (this.unsavedChanges) {
this.$confirm(this.text('unsaved changes'), this.text('unsaved changes title'), {
confirmButtonText: this.text('Go back anyway'),
cancelButtonText: this.text('Stay on page'),
type: 'warning'
}).then(() => { leavePage() }, () => { stayOnPage() })
} else {
leavePage()
}
return
} else if (e.type === 'request-submit-page') {
// User clicked one of the submit buttons.
// If nothing has changed, just submit.
if (!this.unsavedChanges) {
leavePage()
return
}
// Validate destination field (only if the app is visible).
// TODO && !appIsHidden
if (!this.destinationIsValid) {
dispatch(this.$root.$el, 'cancel-leave-page')
stayOnPage()
this.showErrors = true
return
}
// TODO persist data.
// call dispatch(this.$root.$el, 'resume-leave-page') in callback after server responded ok
// or cancel-leave-page + warning in case of http error
this.persistData().then(() => {
leavePage()
}, () => {
stayOnPage()
this.$alert(this.text('service unavailable'), this.text('service unavailable title'), {
confirmButtonText: this.text('OK')
})
})
}
this.askingToLeave = true
}
this.$root.$el.addEventListener('request-submit-page', listener)
this.$root.$el.addEventListener('request-leave-page', listener)
Expand All @@ -99,26 +154,41 @@ export default {
newRedirect () {
this.$root.$emit('newRedirect')
},
persistData () {
return new Promise((resolve, reject) => {
var redirects = clone(this.redirects)
redirects.push(clone(this.defaultRedirect))
api.postData({
url: this.$root.$options.settings.endpoints.redirects,
data: {redirects},
headers: {}
}).then(() => {
resolve()
}, error => {
reject(error)
})
})
},
text (text) {
switch (text) {
case 'Add redirect': return Drupal.t('Add personalized redirect')
case 'Default redirect': return Drupal.t('Default redirect')
case 'type a node title or ID or paste a URL': return Drupal.t('type a node title or ID or paste a URL')
case 'Type to search nodes': return Drupal.t('Type to search nodes')
case 'destination error': return Drupal.t('Please enter a valid URL or choose a node.')
case 'service unavailable title': return Drupal.t('Service unavailable')
case 'service unavailable': return Drupal.t('The service is temporarily unavailable.\rYour redirects could not be saved.\rPlease try again or contact support if the issue persists.')
case 'unsaved changes title': return Drupal.t('Unsaved changes')
case 'unsaved changes': return Drupal.t('You have unsaved changes!\rYou will lose your changes if you go back.')
case 'OK': return Drupal.t('OK')
case 'Cancel': return Drupal.t('Cancel')
case 'Go back anyway': return Drupal.t('Go back anyway')
case 'Stay on page': return Drupal.t('Stay on page')
}
}
}
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Expand Up @@ -19,8 +19,8 @@
:show-dropdown-on-focus="true"
data-key="values"
label-key="label"
url="http://foo.bar.com"
:headers="{'Authorization': 'JWT foo.bar.3456ß8971230469827456.jklcnfgb'}"
:url="$root.$options.settings.endpoints.nodes"
:headers="{}"
search-param="q"
:count="20"
@input="item => {destination = item}"
Expand All @@ -29,7 +29,7 @@
</section>

<FilterEditor
:fields="filterFields"
:fields="$root.$options.settings.fields"
:filters.sync="currentRedirect.filters"
:operators="OPERATORS"
/>
Expand Down Expand Up @@ -103,8 +103,7 @@ export default {
},
...mapState([
'redirects',
'currentRedirectIndex',
'filterFields'
'currentRedirectIndex'
])
},
Expand Down
3 changes: 1 addition & 2 deletions campaignion_wizard/redirects_app/src/main.js
Expand Up @@ -2,7 +2,6 @@
// standalone) has been set in webpack.dev.conf and webpack.test.conf with an alias.
import Vue from 'vue'
import App from './App'
import axios from 'axios'
import {createStore} from './store'

import {
Expand Down Expand Up @@ -31,7 +30,6 @@ Vue.use(DropdownMenu)
Vue.use(Option)
Vue.use(Select)

Vue.prototype.$http = axios
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm
Expand All @@ -50,6 +48,7 @@ containers.forEach(drupalContainer => {
new Vue({
el,
drupalContainer,
settings: {},
template: '<App/>',
store: createStore(),
components: {App}
Expand Down
14 changes: 14 additions & 0 deletions campaignion_wizard/redirects_app/src/store/mutations.js
Expand Up @@ -2,6 +2,20 @@ import Vue from 'vue'
import {clone} from '@/utils'

export default {
initData (state, {redirects, defaultRedirectUrl}) {
if (defaultRedirectUrl && (typeof redirects === 'undefined' || !redirects.length)) {
state.defaultRedirect.destination = defaultRedirectUrl
state.defaultRedirect.prettyDestination = defaultRedirectUrl
} else {
state.defaultRedirect = clone(redirects[redirects.length - 1])
state.redirects = clone(redirects).slice(0, -1)
}

// Preserve initial state
state.initialData.redirects = clone(state.redirects)
state.initialData.defaultRedirect = clone(state.defaultRedirect)
},

editNewRedirect (state) {
state.currentRedirectIndex = -1
},
Expand Down

0 comments on commit 7c8f9e4

Please sign in to comment.