Initial Release
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
(() => {
|
||||
// Support Functions
|
||||
const trimDataURL = (s, x = undefined) => s.slice(s.indexOf(',') + 1, x)
|
||||
const redirectsToString = s => s.sort().join()
|
||||
function PrepareTemplate(selector) {
|
||||
const elem = $(selector)
|
||||
elem.removeAttribute('hidden')
|
||||
elem.removeAttribute('id')
|
||||
elem.remove()
|
||||
return elem
|
||||
}
|
||||
|
||||
// No Applications Warning
|
||||
let appCount = 0
|
||||
function DisplayNoApps(newCount) {
|
||||
_ActionCreate.removeAttribute('hidden')
|
||||
appCount = newCount
|
||||
appCount === 0
|
||||
? _AlertEmpty.removeAttribute('hidden')
|
||||
: _AlertEmpty.setAttribute('hidden', true)
|
||||
}
|
||||
|
||||
// Displays Modal with Secret Key
|
||||
function DisplaySecret(secretKey) {
|
||||
CreateModal({
|
||||
behaviour: {
|
||||
title: 'Your Secret Key',
|
||||
tooltip: 'Do not share this key with anyone!',
|
||||
submitText: 'Close',
|
||||
cancelText: '',
|
||||
},
|
||||
_secret: {
|
||||
type: 'text',
|
||||
title: '',
|
||||
value: secretKey,
|
||||
readonly: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Use Application Template
|
||||
const
|
||||
_AlertEmpty = $('#alert-empty'),
|
||||
_AlertLoad = $('#alert-load'),
|
||||
_ActionCreate = $('#action-create'),
|
||||
_Collection = $('#application-collection'),
|
||||
_LinkTemplate = PrepareTemplate('.input-redirect'),
|
||||
_ItemTemplate = PrepareTemplate('#application-template')
|
||||
|
||||
function DisplayApplication(a) {
|
||||
let removingIcon = false
|
||||
let unsavedChanges = false
|
||||
const
|
||||
item = _ItemTemplate.cloneNode(true),
|
||||
elemStatus = $$(item, '#status-message'),
|
||||
elemOptions = $$(item, '.application-options'),
|
||||
elemRedirects = $$(item, '#container-redirects'),
|
||||
previewIcon = $$(item, '#app-icon'),
|
||||
previewName = $$(item, '#app-name'),
|
||||
previewDesc = $$(item, '#app-desc'),
|
||||
inputName = $$(item, '#input-name'),
|
||||
inputDesc = $$(item, '#input-desc'),
|
||||
inputIcon = $$(item, '#input-icon'),
|
||||
inputIconFile = $$(item, '#input-icon-file'),
|
||||
inputIconRemove = $$(item, '#input-icon-remove'),
|
||||
inputRedirectNew = $$(item, '#input-redirect-new'),
|
||||
inputSave = $$(item, '#input-save'),
|
||||
inputDiscard = $$(item, '#input-discard'),
|
||||
inputResetSecret = $$(item, '#input-reset'),
|
||||
inputDelete = $$(item, '#input-delete'),
|
||||
inputEdit = $$(item, '#input-edit')
|
||||
|
||||
function CreateRedirect(givenURL) {
|
||||
const elem = _LinkTemplate.cloneNode(true)
|
||||
const inputUrl = elem.firstChild
|
||||
inputUrl.value = givenURL
|
||||
inputUrl.addEventListener('change', UpdateForm)
|
||||
elem.lastChild.addEventListener('click', () => {
|
||||
elem.remove()
|
||||
UpdateForm()
|
||||
})
|
||||
elemRedirects.appendChild(elem)
|
||||
}
|
||||
function ResetForm() {
|
||||
removingIcon = false
|
||||
previewName.textContent = a.name?.trim() || 'Untitled'
|
||||
previewDesc.textContent = a.description?.trim() || 'No Description Provided.'
|
||||
previewIcon.src = ImageURL('icons', a.id, a.icon, false, 128)
|
||||
previewIcon.alt = `Icon for ${a.name}`
|
||||
inputIconFile.value = null
|
||||
inputDesc.value = a.description
|
||||
inputName.value = a.name
|
||||
elemRedirects.innerHTML = ''
|
||||
a.redirects.forEach(CreateRedirect)
|
||||
UpdateForm()
|
||||
}
|
||||
function UpdateForm() {
|
||||
// New Redirect
|
||||
elemRedirects.childElementCount >= 10
|
||||
? inputRedirectNew.setAttribute('hidden', true)
|
||||
: inputRedirectNew.removeAttribute('hidden')
|
||||
|
||||
// Remove Icon
|
||||
!removingIcon && (a.icon !== null || inputIconFile.files.length > 0)
|
||||
? inputIconRemove.removeAttribute('disabled')
|
||||
: inputIconRemove.setAttribute('disabled', true)
|
||||
|
||||
// Save/Discard Changes
|
||||
unsavedChanges =
|
||||
Object.keys(CollectBody()).length &&
|
||||
Array.from(elemRedirects.childNodes).every(e => e.firstChild.checkValidity())
|
||||
if (unsavedChanges) {
|
||||
inputDiscard.removeAttribute('disabled')
|
||||
inputSave.removeAttribute('disabled')
|
||||
} else {
|
||||
inputDiscard.setAttribute('disabled', true)
|
||||
inputSave.setAttribute('disabled', true)
|
||||
}
|
||||
}
|
||||
function CollectBody() {
|
||||
const body = {}
|
||||
if ((inputName.value.trim() || a.name) !== a.name)
|
||||
body['name'] = inputName.value
|
||||
if (inputDesc.value.trim() !== (a.description || ''))
|
||||
body['description'] = inputDesc.value
|
||||
if (removingIcon && body.icon)
|
||||
body['icon'] = ''
|
||||
if (inputIconFile.files.length)
|
||||
body['icon'] = trimDataURL(previewIcon.src)
|
||||
const collectedRedirects = Array
|
||||
.from(elemRedirects.childNodes)
|
||||
.filter(e => e.firstChild.checkValidity())
|
||||
.map(e => e.firstChild.value)
|
||||
if (redirectsToString(collectedRedirects) !== redirectsToString(a.redirects)) {
|
||||
body.redirects = collectedRedirects
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
// Set/Remove Application Icon
|
||||
inputIconRemove.addEventListener('click', () => {
|
||||
previewIcon.src = ImageURL('icon', a.id, null)
|
||||
inputIconFile.value = null
|
||||
removingIcon = true
|
||||
UpdateForm()
|
||||
})
|
||||
inputIconFile.addEventListener('change', ev => {
|
||||
const file = ev.target.files[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
previewIcon.src = e.target.result
|
||||
removingIcon = false
|
||||
UpdateForm()
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
// Field Updating
|
||||
inputName.addEventListener('input', () => previewName.textContent = inputName.value.trim() || a.name)
|
||||
inputDesc.addEventListener('input', () => previewDesc.textContent = inputDesc.value.trim() || a.description)
|
||||
for (const needsListener of [inputName, inputDesc]) {
|
||||
needsListener.addEventListener('change', UpdateForm)
|
||||
}
|
||||
// Button Listeners
|
||||
inputRedirectNew.addEventListener('click',
|
||||
() => CreateRedirect('')
|
||||
)
|
||||
inputDiscard.addEventListener('click',
|
||||
() => ResetForm()
|
||||
)
|
||||
inputIcon.addEventListener('click',
|
||||
() => inputIconFile.click()
|
||||
)
|
||||
inputEdit.addEventListener('click', () => {
|
||||
elemOptions.classList.toggle('options-hidden')
|
||||
ResetForm()
|
||||
})
|
||||
inputSave.addEventListener('click', () => {
|
||||
if (!unsavedChanges) return
|
||||
const unlock = LockInteraction()
|
||||
elemStatus.textContent = 'Saving Changes...'
|
||||
SendRequest('PATCH', `/api/v1/users/@me/applications/${a.id}`, CollectBody(), 1)
|
||||
.then(([_, patchedBody]) => {
|
||||
elemStatus.textContent = 'Changes Saved!'
|
||||
elemStatus.classList.remove('text-error')
|
||||
a = patchedBody
|
||||
unlock()
|
||||
ResetForm()
|
||||
})
|
||||
.catch(e => {
|
||||
elemStatus.textContent = e
|
||||
elemStatus.classList.add('text-error')
|
||||
unlock()
|
||||
})
|
||||
// we dont use finally block here because race condition :p
|
||||
})
|
||||
inputResetSecret.addEventListener('click', () => CreateModal({
|
||||
behaviour: {
|
||||
title: 'Reset Secret Key?',
|
||||
tooltip: `${a.name} will stop functioning until you update the secret key in your code.`,
|
||||
submitText: 'Reset',
|
||||
onSubmit: () => new Promise(ok => {
|
||||
SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}/reset`, undefined, 2)
|
||||
.then(([_, body]) => {
|
||||
DisplaySecret(body.secret_key)
|
||||
ok()
|
||||
})
|
||||
.catch(ok)
|
||||
})
|
||||
}
|
||||
}))
|
||||
inputDelete.addEventListener('click', () => CreateModal({
|
||||
behaviour: {
|
||||
title: 'Delete Application?',
|
||||
tooltip: `${a.name} will be deleted and all connections will be severed. This action cannot be undone.`,
|
||||
submitText: 'Delete',
|
||||
onSubmit: () => new Promise(ok => {
|
||||
SendRequest('DELETE', `/api/v1/users/@me/applications/${a.id}`, undefined, 2)
|
||||
.then(() => {
|
||||
item.remove()
|
||||
DisplayNoApps(appCount - 1)
|
||||
ok()
|
||||
})
|
||||
.catch(ok)
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
// Append to top
|
||||
// Official Application?
|
||||
if ((a.flags & 1 << 3) !== 0) {
|
||||
$$(item, '#verified-message').removeAttribute('hidden')
|
||||
}
|
||||
$$(item, '#input-clientid').textContent += a.id
|
||||
ResetForm()
|
||||
_Collection.appendChild(item)
|
||||
}
|
||||
|
||||
// Fetch Connections
|
||||
|
||||
SendRequest('GET', '/api/v1/users/@me/applications', null, 1)
|
||||
.then(([_, body]) => {
|
||||
// Display Applications
|
||||
body
|
||||
.sort((a, b) => new Date(b.created) - new Date(a.created))
|
||||
.forEach(DisplayApplication)
|
||||
DisplayNoApps(body.length)
|
||||
|
||||
// Bind Create Application
|
||||
_ActionCreate.addEventListener('click', function ActionCreate() {
|
||||
CreateModal({
|
||||
behaviour: {
|
||||
title: 'Create Application',
|
||||
tooltip: 'Name it something cool like Phase Connect.',
|
||||
onSubmit: ({ name }) => new Promise(ok => {
|
||||
SendRequest('POST', '/api/v1/users/@me/applications', { name }, 1)
|
||||
.then(([_, body]) => {
|
||||
DisplaySecret(body.secret_key)
|
||||
DisplayNoApps(appCount + 1)
|
||||
DisplayApplication(body)
|
||||
ok()
|
||||
})
|
||||
.catch(ok)
|
||||
})
|
||||
},
|
||||
_divider: { type: 'divider-line' },
|
||||
name: {
|
||||
type: 'text',
|
||||
required: true,
|
||||
title: 'Name',
|
||||
validator: function ValidateName(v) {
|
||||
if (v.length > 24) return 'Application name cannot be longer than 32 characters'
|
||||
if (v.length < 1) return 'Application name cannot be shorter than 1 character'
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
// Display Error Message
|
||||
$('#alert-error').removeAttribute('hidden')
|
||||
$('#error-message').textContent = e
|
||||
console.log(e)
|
||||
})
|
||||
.finally(() => {
|
||||
// Hide Loading Symbol
|
||||
_AlertLoad.setAttribute('hidden', true)
|
||||
})
|
||||
})()
|
||||
Reference in New Issue
Block a user