app/javascript/vue/components/radials/navigation/radial.vue
<template>
<div v-if="!deleted">
<div class="radial-annotator">
<VModal
v-if="isRadialOpen"
transparent
@close="closeModal()"
>
<template #header>
<span class="flex-separate middle">
<span v-html="title" />
<b
v-if="metadata"
class="separate-right"
v-text="metadata.type"
/>
</span>
</template>
<template #body>
<div class="horizontal-center-content">
<spinner v-if="isLoading" />
<RadialMenu
v-if="metadata"
ref="radialElement"
:options="menuOptions"
@onClick="selectedRadialOption"
/>
<DestroyConfirmation
v-if="showDestroyModal"
@close="showDestroyModal = false"
@confirm="destroyObject"
/>
</div>
</template>
</VModal>
<AllTasks
v-if="isAlltaskSelected"
@close="isAlltaskSelected = false"
:metadata="metadata"
/>
<VBtn
v-if="showBottom"
:title="buttonTitle"
color="radial"
circle
:disabled="disabled"
@click="openRadialMenu()"
>
<VIcon
:title="buttonTitle"
name="radialNavigator"
x-small
/>
</VBtn>
</div>
</div>
</template>
<script setup>
import RadialMenu from '@/components/radials/RadialMenu.vue'
import Spinner from '@/components/ui/VSpinner.vue'
import VBtn from '@/components/ui/VBtn/index.vue'
import VIcon from '@/components/ui/VIcon/index.vue'
import Icons from './images/icons.js'
import DestroyConfirmation from './components/DestroyConfirmation'
import AllTasks from './components/allTasks.vue'
import ajaxCall from '@/helpers/ajaxCall'
import { PinboardItem } from '@/routes/endpoints'
import { computed, ref, watch } from 'vue'
import VModal from '@/components/ui/Modal.vue'
const DEFAULT_OPTIONS = {
New: 'New',
Edit: 'Edit',
Destroy: 'Destroy',
Recent: 'Recent',
Show: 'Show',
Related: 'Related'
}
const CUSTOM_OPTIONS = {
AllTasks: 'allTasks',
CircleButton: 'circleButton'
}
const props = defineProps({
globalId: {
type: String,
required: true
},
showBottom: {
type: Boolean,
default: true
},
buttonTitle: {
type: String,
default: 'Radial navigator'
},
maxTaskInPie: {
type: Number,
default: 4
},
components: {
type: Object,
default: () => ({})
},
exclude: {
type: [String, Array],
default: () => []
},
disabled: {
type: Boolean,
default: false
},
redirect: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['close', 'delete'])
const defaultTasks = computed(() => ({
graph_object: {
name: 'Object graph',
path: `/tasks/graph/object?global_id=${encodeURIComponent(props.globalId)}`
}
}))
const menuOptions = computed(() => {
const tasks = metadata.value.tasks || {}
const taskSlices = Object.entries(tasks)
.slice(0, props.maxTaskInPie)
.map(([task, { name, path }]) => ({
name: task,
label: name,
link: path,
icon: Icons[task]
? {
url: Icons[task],
width: '20',
height: '20'
}
: undefined
}))
if (Object.keys(tasks).length > props.maxTaskInPie) {
taskSlices.push({
label: 'All tasks',
name: CUSTOM_OPTIONS.AllTasks,
svgAttributes: {
class: 'slice'
},
icon: {
url: Icons.AllTasks,
width: '20',
height: '20'
}
})
}
const slices = [...taskSlices, ...defaultSlices.value]
return {
width: 500,
height: 500,
sliceSize: 190,
innerPosition: 1.4,
centerSize: 34,
margin: 0,
middleButton: middleButton.value,
svgAttributes: {
class: 'svg-radial-menu svg-radial-menu-navigator'
},
svgSliceAttributes: {
fontSize: 11,
class: 'slice'
},
slices
}
})
const defaultSlices = computed(() => {
const exclude = [props.exclude].flat()
if (!metadata.value.destroy) {
exclude.push(addSlice(DEFAULT_OPTIONS.Destroy))
}
return defaultSlicesTypes
.filter((type) => !exclude.includes(type))
.map((type) => addSlice(type, { link: defaultLinks()[type] }))
})
const isPinned = computed(() => metadata.value?.pinboard_item)
const middleButton = computed(() => ({
name: CUSTOM_OPTIONS.CircleButton,
radius: 30,
icon: {
url: Icons.Pin,
width: '20',
height: '20'
},
svgAttributes: {
fill: isPinned.value ? '#F44336' : '#9ccc65'
}
}))
const isLoading = ref(false)
const isAlltaskSelected = ref(false)
const isRadialOpen = ref(false)
const globalIdSaved = ref(undefined)
const metadata = ref(undefined)
const title = ref('Radial navigation')
const deleted = ref(false)
const showDestroyModal = ref(false)
const radialElement = ref(null)
const defaultSlicesTypes = [
DEFAULT_OPTIONS.Related,
DEFAULT_OPTIONS.New,
DEFAULT_OPTIONS.Destroy,
DEFAULT_OPTIONS.Edit,
DEFAULT_OPTIONS.Show
]
watch(radialElement, (newVal) => {
if (newVal) {
newVal.$el.querySelectorAll('a').forEach((element) => {
element.addEventListener('click', (event) => {
const isShortcutKeyPressed =
event.ctrlKey || event.shiftKey || event.metaKey
if (isShortcutKeyPressed) {
isRadialOpen.value = false
}
})
})
}
})
function addSlice(type, attr) {
return {
label: type,
name: type,
radius: 30,
icon: {
url: Icons[type],
width: '20',
height: '20'
},
svgAttributes: {
class: 'slice'
},
...attr
}
}
function selectedRadialOption({ name }) {
switch (name) {
case CUSTOM_OPTIONS.CircleButton:
isPinned.value ? destroyPin() : createPin()
break
case DEFAULT_OPTIONS.Destroy:
showDestroyModal.value = true
break
case CUSTOM_OPTIONS.AllTasks:
isAlltaskSelected.value = true
break
}
}
function defaultLinks() {
return {
[DEFAULT_OPTIONS.Edit]:
metadata.value?.edit || `${metadata.value?.resource_path}/edit`,
[DEFAULT_OPTIONS.New]:
metadata.value?.new ||
`${metadata.value.resource_path.substring(
0,
metadata.value.resource_path.lastIndexOf('/')
)}/new`,
[DEFAULT_OPTIONS.Show]: metadata.value.resource_path,
[DEFAULT_OPTIONS.Related]: `/tasks/shared/related_data?object_global_id=${encodeURIComponent(
props.globalId
)}`
}
}
function closeModal() {
isRadialOpen.value = false
eventClose()
emit('close')
}
function openRadialMenu() {
isRadialOpen.value = true
loadMetadata(props.globalId)
}
function loadMetadata(globalId) {
if (globalId === globalIdSaved.value && metadata.value) return
globalIdSaved.value = globalId
isLoading.value = true
ajaxCall(
'get',
`/metadata/object_radial?global_id=${encodeURIComponent(globalId)}`
).then(({ body }) => {
const { tasks, ...rest } = body
metadata.value = rest
metadata.value.tasks = {
...tasks,
...defaultTasks.value
}
title.value = metadata.value.object_label
isLoading.value = false
})
}
/* function splitLongWords(taskName) {
const totalTasks = Object.keys(metadata.value.tasks).length
const maxPerLine = totalTasks > 4 ? 8 : 16
const arr = taskName.split(' ')
const words = []
arr.forEach((word) => {
const wordLength = word.length
const wordArr = []
for (let i = 0; i < wordLength; i += maxPerLine) {
wordArr.push(word.slice(i, maxPerLine + i))
}
words.push(wordArr.join('- '))
})
return words.join(' ')
} */
function eventClose() {
const event = new CustomEvent('radialObject:close', {
detail: {
metadata: metadata.value
}
})
document.dispatchEvent(event)
}
function eventDestroy() {
const event = new CustomEvent('radialObject:destroy', {
detail: {
metadata: metadata.value
}
})
document.dispatchEvent(event)
}
function createPin() {
const payload = {
pinned_object_id: metadata.value.id,
pinned_object_type: metadata.value.type,
is_inserted: true
}
PinboardItem.create({ pinboard_item: payload }).then(({ body }) => {
metadata.value.pinboard_item = { id: body.id }
TW.workbench.pinboard.addToPinboard(body)
TW.workbench.alert.create(
'Pinboard item was successfully created.',
'notice'
)
})
}
function destroyPin() {
PinboardItem.destroy(metadata.value.pinboard_item.id).then((_) => {
TW.workbench.alert.create(
'Pinboard item was successfully destroyed.',
'notice'
)
TW.workbench.pinboard.removeItem(metadata.value.pinboard_item.id)
delete metadata.value.pinboard_item
})
}
function destroyObject() {
showDestroyModal.value = false
ajaxCall('delete', `${metadata.value.resource_path}.json`)
.then((_) => {
TW.workbench.alert.create(
`${metadata.value.type} was successfully destroyed.`,
'notice'
)
if (props.globalId === metadata.value.global_id) {
eventDestroy()
deleted.value = true
}
if (props.redirect) {
if (metadata.value.destroyed_redirect) {
window.open(metadata.value.destroyed_redirect, '_self')
} else if (window.location.pathname === metadata.value.resource_path) {
window.open(`/${window.location.pathname.split('/')[1]}`, '_self')
} else {
window.open(
metadata.value.resource_path.substring(
0,
metadata.value.resource_path.lastIndexOf('/')
),
'_self'
)
}
}
emit('delete', metadata.value)
closeModal()
})
.catch(() => {})
}
</script>
<script>
export default {
name: 'RadialNavigation'
}
</script>
<style>
.svg-radial-menu-navigator path {
stroke: #444;
stroke-width: 2px;
}
</style>