feat(frontend): adapt QuickActions for quick-add mode behavior
This commit is contained in:
parent
9072ca84d5
commit
8dc96d61bd
|
|
@ -4,7 +4,12 @@
|
|||
:overflow="isNewTaskCommand"
|
||||
@close="closeQuickActions"
|
||||
>
|
||||
<div class="card quick-actions">
|
||||
<div
|
||||
ref="quickActionsCard"
|
||||
class="card quick-actions"
|
||||
:class="{'is-quick-add-mode': isQuickAddMode}"
|
||||
:style="isQuickAddMode ? {maxHeight: quickEntryMaxHeight + 'px', overflowY: 'auto'} : undefined"
|
||||
>
|
||||
<div
|
||||
class="action-input"
|
||||
:class="{'has-active-cmd': selectedCmd !== null}"
|
||||
|
|
@ -28,6 +33,9 @@
|
|||
@keyup.prevent.enter="doCmd"
|
||||
@keyup.prevent.esc="closeQuickActions"
|
||||
>
|
||||
<QuickAddMagic
|
||||
v-if="isNewTaskCommand"
|
||||
/>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="closeQuickActions"
|
||||
|
|
@ -43,8 +51,6 @@
|
|||
{{ hintText }}
|
||||
</div>
|
||||
|
||||
<QuickAddMagic v-if="isNewTaskCommand" />
|
||||
|
||||
<div
|
||||
v-if="selectedCmd === null"
|
||||
class="results"
|
||||
|
|
@ -97,7 +103,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type ComponentPublicInstance, computed, ref, shallowReactive, watchEffect} from 'vue'
|
||||
import {type ComponentPublicInstance, computed, ref, shallowReactive, watch, watchEffect, onBeforeUnmount} from 'vue'
|
||||
import {useQuickAddMode} from '@/composables/useQuickAddMode'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
|
|
@ -137,6 +144,8 @@ const labelStore = useLabelStore()
|
|||
const taskStore = useTaskStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const {isQuickAddMode} = useQuickAddMode()
|
||||
|
||||
type DoAction<Type> = { type: ACTION_TYPE } & Type
|
||||
|
||||
enum ACTION_TYPE {
|
||||
|
|
@ -177,6 +186,37 @@ watchEffect(() => {
|
|||
}
|
||||
})
|
||||
|
||||
let focusRafId: number | null = null
|
||||
|
||||
watchEffect(() => {
|
||||
if (active.value && isQuickAddMode) {
|
||||
selectedCmd.value = commands.value.newTask
|
||||
|
||||
// The input may not be focusable yet due to:
|
||||
// 1. Modal transition (v-if + <Transition appear>) delaying DOM readiness
|
||||
// 2. Electron window not yet visible (shown after did-finish-load)
|
||||
// Retry with rAF until focus actually lands on the input.
|
||||
const tryFocus = () => {
|
||||
if (!active.value) {
|
||||
focusRafId = null
|
||||
return
|
||||
}
|
||||
if (searchInput.value) {
|
||||
searchInput.value.focus()
|
||||
if (document.activeElement === searchInput.value) {
|
||||
focusRafId = null
|
||||
return
|
||||
}
|
||||
}
|
||||
focusRafId = requestAnimationFrame(tryFocus)
|
||||
}
|
||||
focusRafId = requestAnimationFrame(tryFocus)
|
||||
} else if (focusRafId !== null) {
|
||||
cancelAnimationFrame(focusRafId)
|
||||
focusRafId = null
|
||||
}
|
||||
})
|
||||
|
||||
function closeQuickActions() {
|
||||
baseStore.setQuickActionsActive(false)
|
||||
}
|
||||
|
|
@ -297,7 +337,7 @@ const currentProject = computed(() => {
|
|||
if (Object.keys(baseStore.currentProject).length === 0 || isSavedFilter(baseStore.currentProject)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
return baseStore.currentProject
|
||||
})
|
||||
|
||||
|
|
@ -454,29 +494,66 @@ function search() {
|
|||
}
|
||||
|
||||
const searchInput = ref<HTMLElement | null>(null)
|
||||
const quickActionsCard = ref<HTMLElement | null>(null)
|
||||
|
||||
const QUICK_ENTRY_WIDTH = 680
|
||||
const quickEntryMaxHeight = Math.round(window.screen.availHeight * 0.7)
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
if (isQuickAddMode) {
|
||||
watch(quickActionsCard, (el) => {
|
||||
resizeObserver?.disconnect()
|
||||
if (!el) return
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const height = Math.min(
|
||||
Math.ceil(entry.borderBoxSize[0].blockSize),
|
||||
quickEntryMaxHeight,
|
||||
)
|
||||
window.quickEntry?.resize?.(QUICK_ENTRY_WIDTH, height)
|
||||
}
|
||||
})
|
||||
resizeObserver.observe(el)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
})
|
||||
}
|
||||
|
||||
async function doAction(type: ACTION_TYPE, item: DoAction) {
|
||||
switch (type) {
|
||||
case ACTION_TYPE.PROJECT:
|
||||
closeQuickActions()
|
||||
await router.push({
|
||||
name: 'project.index',
|
||||
params: {projectId: (item as DoAction<IProject>).id},
|
||||
})
|
||||
if (!isQuickAddMode) {
|
||||
await router.push({
|
||||
name: 'project.index',
|
||||
params: {projectId: (item as DoAction<IProject>).id},
|
||||
})
|
||||
}
|
||||
break
|
||||
case ACTION_TYPE.TASK:
|
||||
if (isQuickAddMode) {
|
||||
const channel = new BroadcastChannel('vikunja-task-updates')
|
||||
channel.postMessage({type: 'task-created-open', taskId: (item as DoAction<ITask>).id})
|
||||
channel.close()
|
||||
window.quickEntry?.showMainWindow()
|
||||
} else {
|
||||
await router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: (item as DoAction<ITask>).id},
|
||||
})
|
||||
}
|
||||
closeQuickActions()
|
||||
await router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: (item as DoAction<ITask>).id},
|
||||
})
|
||||
break
|
||||
case ACTION_TYPE.TEAM:
|
||||
closeQuickActions()
|
||||
await router.push({
|
||||
name: 'teams.edit',
|
||||
params: {id: (item as DoAction<ITeam>).id},
|
||||
})
|
||||
if (!isQuickAddMode) {
|
||||
await router.push({
|
||||
name: 'teams.edit',
|
||||
params: {id: (item as DoAction<ITeam>).id},
|
||||
})
|
||||
}
|
||||
break
|
||||
case ACTION_TYPE.CMD:
|
||||
query.value = ''
|
||||
|
|
@ -506,7 +583,9 @@ async function doCmd() {
|
|||
return
|
||||
}
|
||||
|
||||
closeQuickActions()
|
||||
if (!isQuickAddMode) {
|
||||
closeQuickActions()
|
||||
}
|
||||
await selectedCmd.value.action()
|
||||
}
|
||||
|
||||
|
|
@ -520,6 +599,15 @@ async function newTask() {
|
|||
projectId,
|
||||
})
|
||||
success({message: t('task.createSuccess')})
|
||||
|
||||
if (isQuickAddMode) {
|
||||
const channel = new BroadcastChannel('vikunja-task-updates')
|
||||
channel.postMessage({type: 'task-created', taskId: task.id})
|
||||
channel.close()
|
||||
closeQuickActions()
|
||||
return
|
||||
}
|
||||
|
||||
await router.push({name: 'task.detail', params: {id: task.id}})
|
||||
}
|
||||
|
||||
|
|
@ -602,6 +690,13 @@ function reset() {
|
|||
inset-block-start: 3rem;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.is-quick-add-mode {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.action-input {
|
||||
|
|
@ -611,7 +706,7 @@ function reset() {
|
|||
.input {
|
||||
border: 0;
|
||||
font-size: 1.5rem;
|
||||
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
padding-inline-end: .25rem;
|
||||
}
|
||||
|
|
@ -624,7 +719,7 @@ function reset() {
|
|||
.close {
|
||||
padding: 0 1rem 0 .5rem;
|
||||
font-size: 1.5rem;
|
||||
|
||||
|
||||
@media screen and (min-width: $tablet + 1) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -675,14 +770,14 @@ function reset() {
|
|||
&:active {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
|
||||
|
||||
.saved-filter-icon {
|
||||
font-size: .75rem;
|
||||
inline-size: .75rem;
|
||||
margin-inline-end: .25rem;
|
||||
color: var(--grey-400)
|
||||
}
|
||||
|
||||
|
||||
&:has(.saved-filter-icon) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -33,5 +33,25 @@ watch(() => baseStore.quickActionsActive, (active) => {
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// In quick-add mode the Electron window IS the overlay – hide the modal
|
||||
// backdrop, disable scroll, and collapse all extra spacing so the input
|
||||
// fills the window edge-to-edge.
|
||||
.quick-add-overlay {
|
||||
.modal-mask {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-mask > .close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,101 +1,110 @@
|
|||
<template>
|
||||
<template v-if="mode !== 'disabled' && prefixes !== undefined">
|
||||
<BaseButton
|
||||
v-tooltip="$t('task.quickAddMagic.hint')"
|
||||
class="icon is-small show-helper-text quick-add-magic-trigger-btn"
|
||||
:aria-label="$t('task.quickAddMagic.hint')"
|
||||
:class="{'is-highlighted': highlightHintIcon}"
|
||||
@click="() => visible = true"
|
||||
<span
|
||||
v-if="isQuickAddMode"
|
||||
v-tooltip="$t('task.quickAddMagic.quickEntryHint')"
|
||||
class="icon is-small show-helper-text"
|
||||
>
|
||||
<Icon :icon="['far', 'circle-question']" />
|
||||
</BaseButton>
|
||||
<Modal
|
||||
:enabled="visible"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="() => visible = false"
|
||||
>
|
||||
<Card
|
||||
class="has-no-shadow"
|
||||
:title="$t('task.quickAddMagic.title')"
|
||||
:show-close="true"
|
||||
@close="() => visible = false"
|
||||
</span>
|
||||
<template v-else>
|
||||
<BaseButton
|
||||
v-tooltip="$t('task.quickAddMagic.hint')"
|
||||
class="icon is-small show-helper-text quick-add-magic-trigger-btn"
|
||||
:aria-label="$t('task.quickAddMagic.hint')"
|
||||
:class="{'is-highlighted': highlightHintIcon}"
|
||||
@click="open"
|
||||
>
|
||||
<p>{{ $t('task.quickAddMagic.intro') }}</p>
|
||||
<Icon :icon="['far', 'circle-question']" />
|
||||
</BaseButton>
|
||||
<Modal
|
||||
:enabled="visible"
|
||||
transition-name="fade"
|
||||
:overflow="true"
|
||||
variant="hint-modal"
|
||||
@close="close"
|
||||
>
|
||||
<Card
|
||||
class="has-no-shadow"
|
||||
:title="$t('task.quickAddMagic.title')"
|
||||
:show-close="true"
|
||||
@close="close"
|
||||
>
|
||||
<p>{{ $t('task.quickAddMagic.intro') }}</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.labels') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label1', {prefix: prefixes.label}) }}
|
||||
{{ $t('task.quickAddMagic.label2') }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label3') }}
|
||||
{{ $t('task.quickAddMagic.label4', {prefix: prefixes.label}) }}
|
||||
</p>
|
||||
<h3>{{ $t('task.attributes.labels') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label1', {prefix: prefixes.label}) }}
|
||||
{{ $t('task.quickAddMagic.label2') }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.label3') }}
|
||||
{{ $t('task.quickAddMagic.label4', {prefix: prefixes.label}) }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.priority') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.priority1', {prefix: prefixes.priority}) }}
|
||||
{{ $t('task.quickAddMagic.priority2') }}
|
||||
</p>
|
||||
<h3>{{ $t('task.attributes.priority') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.priority1', {prefix: prefixes.priority}) }}
|
||||
{{ $t('task.quickAddMagic.priority2') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.attributes.assignees') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.assignees', {prefix: prefixes.assignee}) }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
<h3>{{ $t('task.attributes.assignees') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.assignees', {prefix: prefixes.assignee}) }}
|
||||
{{ $t('task.quickAddMagic.multiple') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('quickActions.projects') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
|
||||
{{ $t('task.quickAddMagic.project2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.project3') }}
|
||||
{{ $t('task.quickAddMagic.project4', {prefix: prefixes.project}) }}
|
||||
</p>
|
||||
<h3>{{ $t('quickActions.projects') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.project1', {prefix: prefixes.project}) }}
|
||||
{{ $t('task.quickAddMagic.project2') }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.project3') }}
|
||||
{{ $t('task.quickAddMagic.project4', {prefix: prefixes.project}) }}
|
||||
</p>
|
||||
|
||||
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.date') }}
|
||||
</p>
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Today</li>
|
||||
<li>Tonight</li>
|
||||
<li>Tomorrow</li>
|
||||
<li>Next monday</li>
|
||||
<li>This weekend</li>
|
||||
<li>Later this week</li>
|
||||
<li>Later next week</li>
|
||||
<li>Next week</li>
|
||||
<li>Next month</li>
|
||||
<li>End of month</li>
|
||||
<li>In 5 days [hours/weeks/months]</li>
|
||||
<li>Tuesday ({{ $t('task.quickAddMagic.dateWeekday') }})</li>
|
||||
<li>02/17/2021</li>
|
||||
<li>2021-02-17</li>
|
||||
<li>17.02.2021</li>
|
||||
<li>Feb 17 ({{ $t('task.quickAddMagic.dateCurrentYear') }})</li>
|
||||
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
|
||||
</ul>
|
||||
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
|
||||
<h3>{{ $t('task.quickAddMagic.dateAndTime') }}</h3>
|
||||
<p>
|
||||
{{ $t('task.quickAddMagic.date') }}
|
||||
</p>
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Today</li>
|
||||
<li>Tonight</li>
|
||||
<li>Tomorrow</li>
|
||||
<li>Next monday</li>
|
||||
<li>This weekend</li>
|
||||
<li>Later this week</li>
|
||||
<li>Later next week</li>
|
||||
<li>Next week</li>
|
||||
<li>Next month</li>
|
||||
<li>End of month</li>
|
||||
<li>In 5 days [hours/weeks/months]</li>
|
||||
<li>Tuesday ({{ $t('task.quickAddMagic.dateWeekday') }})</li>
|
||||
<li>02/17/2021</li>
|
||||
<li>2021-02-17</li>
|
||||
<li>17.02.2021</li>
|
||||
<li>Feb 17 ({{ $t('task.quickAddMagic.dateCurrentYear') }})</li>
|
||||
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
|
||||
</ul>
|
||||
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
|
||||
|
||||
<h3>{{ $t('task.quickAddMagic.repeats') }}</h3>
|
||||
<p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p>
|
||||
<p>{{ $t('misc.forExample') }}</p>
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Every day</li>
|
||||
<li>Every 3 days</li>
|
||||
<li>Every week</li>
|
||||
<li>Every 2 weeks</li>
|
||||
<li>Every month</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</Modal>
|
||||
<h3>{{ $t('task.quickAddMagic.repeats') }}</h3>
|
||||
<p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p>
|
||||
<p>{{ $t('misc.forExample') }}</p>
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Every day</li>
|
||||
<li>Every 3 days</li>
|
||||
<li>Every week</li>
|
||||
<li>Every 2 weeks</li>
|
||||
<li>Every month</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</Modal>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
|
@ -106,17 +115,34 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
|
||||
import {PREFIXES} from '@/modules/quickAddMagic'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useQuickAddMode} from '@/composables/useQuickAddMode'
|
||||
|
||||
defineProps<{
|
||||
highlightHintIcon?: boolean,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
opened: []
|
||||
closed: []
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const {isQuickAddMode} = useQuickAddMode()
|
||||
|
||||
const visible = ref(false)
|
||||
const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
|
||||
|
||||
const prefixes = computed(() => PREFIXES[mode.value])
|
||||
|
||||
function open() {
|
||||
visible.value = true
|
||||
emit('opened')
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
emit('closed')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,7 @@
|
|||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
|
||||
"quickEntryHint": "Use magic prefixes for dates, labels & more. Open the main Vikunja app and check the tooltip on the task input for more details.",
|
||||
"title": "Quick Add Magic",
|
||||
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
|
||||
"multiple": "You can use this multiple times.",
|
||||
|
|
|
|||
Loading…
Reference in New Issue