Initial commit
This commit is contained in:
237
resources/js/views/apps/kanban/KanbanBoard.vue
Normal file
237
resources/js/views/apps/kanban/KanbanBoard.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup>
|
||||
import {
|
||||
animations,
|
||||
remapNodes,
|
||||
} from '@formkit/drag-and-drop'
|
||||
import { dragAndDrop } from '@formkit/drag-and-drop/vue'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import KanbanBoardEditDrawer from '@/views/apps/kanban/KanbanBoardEditDrawer.vue'
|
||||
import KanbanItems from '@/views/apps/kanban/KanbanItems.vue'
|
||||
|
||||
const props = defineProps({
|
||||
kanbanData: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
groupName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'kanban',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'addNewBoard',
|
||||
'renameBoard',
|
||||
'deleteBoard',
|
||||
'addNewItem',
|
||||
'editItem',
|
||||
'deleteItem',
|
||||
'updateItemsState',
|
||||
'updateBoardState',
|
||||
])
|
||||
|
||||
const kanbanWrapper = ref()
|
||||
const localKanbanData = ref(props.kanbanData.boards)
|
||||
const isKanbanBoardEditVisible = ref(false)
|
||||
const isAddNewFormVisible = ref(false)
|
||||
const refAddNewBoard = ref()
|
||||
const boardTitle = ref('')
|
||||
const editKanbanItem = ref()
|
||||
|
||||
// 👉 Add new board function that emit the name and id of new board
|
||||
const addNewBoard = () => {
|
||||
refAddNewBoard.value?.validate().then(valid => {
|
||||
if (valid.valid) {
|
||||
emit('addNewBoard', boardTitle.value)
|
||||
isAddNewFormVisible.value = false
|
||||
boardTitle.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteBoard = boardId => {
|
||||
emit('deleteBoard', boardId)
|
||||
}
|
||||
|
||||
const renameBoard = boardName => {
|
||||
emit('renameBoard', boardName)
|
||||
}
|
||||
|
||||
const addNewItem = item => {
|
||||
emit('addNewItem', item)
|
||||
}
|
||||
|
||||
const editKanbanItemFn = item => {
|
||||
if (item) {
|
||||
editKanbanItem.value = item
|
||||
isKanbanBoardEditVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const updateStateFn = kanbanState => {
|
||||
emit('updateItemsState', kanbanState)
|
||||
}
|
||||
|
||||
// 👉 initialize the drag and drop
|
||||
dragAndDrop({
|
||||
parent: kanbanWrapper,
|
||||
values: localKanbanData,
|
||||
dragHandle: '.drag-handler',
|
||||
plugins: [animations()],
|
||||
})
|
||||
|
||||
// assign the new kanban data to the local kanban data
|
||||
watch(() => props, () => {
|
||||
localKanbanData.value = props.kanbanData.boards
|
||||
|
||||
// 👉 remap the nodes when we rename the board: https://github.com/formkit/drag-and-drop/discussions/52#discussioncomment-8995203
|
||||
remapNodes(kanbanWrapper.value)
|
||||
}, { deep: true })
|
||||
|
||||
const emitUpdatedTaskFn = item => {
|
||||
emit('editItem', item)
|
||||
}
|
||||
|
||||
const deleteKanbanItemFn = item => {
|
||||
emit('deleteItem', item)
|
||||
}
|
||||
|
||||
// 👉 update boards data when it sort
|
||||
watch(localKanbanData, () => {
|
||||
const getIds = localKanbanData.value.map(board => board.id)
|
||||
|
||||
emit('updateBoardState', getIds)
|
||||
}, { deep: true })
|
||||
|
||||
// 👉 validators for add new board
|
||||
const validateBoardTitle = () => {
|
||||
return props.kanbanData.boards.some(board => boardTitle.value && board.title.toLowerCase() === boardTitle.value.toLowerCase()) ? 'Board title already exists' : true
|
||||
}
|
||||
|
||||
const hideAddNewForm = () => {
|
||||
isAddNewFormVisible.value = false
|
||||
refAddNewBoard.value?.reset()
|
||||
}
|
||||
|
||||
// close add new item form when you loose focus from the form
|
||||
onClickOutside(refAddNewBoard, hideAddNewForm)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kanban-main-wrapper d-flex gap-4 h-100">
|
||||
<!-- 👉 kanban render -->
|
||||
<div
|
||||
ref="kanbanWrapper"
|
||||
class="d-flex ga-6"
|
||||
>
|
||||
<template
|
||||
v-for="kb in localKanbanData"
|
||||
:key="kb.id"
|
||||
>
|
||||
<!-- 👉 kanban task render -->
|
||||
<KanbanItems
|
||||
:group-name="groupName"
|
||||
:kanban-ids="kb.itemsIds"
|
||||
:board-name="kb.title"
|
||||
:board-id="kb.id"
|
||||
:kanban-items="kanbanData.items"
|
||||
:kanban-data="kanbanData"
|
||||
@delete-board="deleteBoard"
|
||||
@rename-board="renameBoard"
|
||||
@add-new-item="addNewItem"
|
||||
@edit-item="editKanbanItemFn"
|
||||
@update-items-state="updateStateFn"
|
||||
@delete-item="deleteKanbanItemFn"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 👉 add new form -->
|
||||
<div
|
||||
class="add-new-form text-no-wrap"
|
||||
style="inline-size: 10rem;"
|
||||
>
|
||||
<h6
|
||||
class="text-lg font-weight-medium cursor-pointer"
|
||||
@click="isAddNewFormVisible = !isAddNewFormVisible"
|
||||
>
|
||||
<VIcon
|
||||
size="18"
|
||||
icon="tabler-plus"
|
||||
/> Add New
|
||||
</h6>
|
||||
|
||||
<!-- 👉 Form -->
|
||||
<VForm
|
||||
v-if="isAddNewFormVisible"
|
||||
ref="refAddNewBoard"
|
||||
class="mt-4"
|
||||
validate-on="submit"
|
||||
@submit.prevent="addNewBoard"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<VTextField
|
||||
v-model="boardTitle"
|
||||
:rules="[requiredValidator, validateBoardTitle]"
|
||||
autofocus
|
||||
placeholder="Add Board Title"
|
||||
@keydown.esc="hideAddNewForm"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex gap-3">
|
||||
<VBtn
|
||||
size="small"
|
||||
type="submit"
|
||||
>
|
||||
Add
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
type="reset"
|
||||
@click="hideAddNewForm"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- kanban edit drawer -->
|
||||
<KanbanBoardEditDrawer
|
||||
v-model:is-drawer-open="isKanbanBoardEditVisible"
|
||||
:kanban-item="editKanbanItem"
|
||||
@update:kanban-item="emitUpdatedTaskFn"
|
||||
@delete-kanban-item="deleteKanbanItemFn"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@use "@styles/variables/_vuetify.scss" as vuetify;
|
||||
|
||||
.kanban-main-wrapper {
|
||||
overflow: auto hidden;
|
||||
margin-inline-start: -0.6rem;
|
||||
min-block-size: calc(100vh - 10.5rem);
|
||||
padding-inline-start: 0.6rem;
|
||||
|
||||
.kanban-board {
|
||||
inline-size: 16.875rem;
|
||||
min-inline-size: 16.875rem;
|
||||
|
||||
.kanban-board-drop-zone {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.add-new-form {
|
||||
.v-field__field {
|
||||
border-radius: vuetify.$border-radius-root;
|
||||
background-color: rgb(var(--v-theme-surface));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
390
resources/js/views/apps/kanban/KanbanBoardEditDrawer.vue
Normal file
390
resources/js/views/apps/kanban/KanbanBoardEditDrawer.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<script setup>
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
import { TextAlign } from '@tiptap/extension-text-align'
|
||||
import { Underline } from '@tiptap/extension-underline'
|
||||
import { StarterKit } from '@tiptap/starter-kit'
|
||||
import {
|
||||
EditorContent,
|
||||
useEditor,
|
||||
} from '@tiptap/vue-3'
|
||||
import { PerfectScrollbar } from 'vue3-perfect-scrollbar'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import avatar1 from '@images/avatars/avatar-1.png'
|
||||
import avatar2 from '@images/avatars/avatar-2.png'
|
||||
import avatar3 from '@images/avatars/avatar-3.png'
|
||||
import avatar4 from '@images/avatars/avatar-4.png'
|
||||
import avatar5 from '@images/avatars/avatar-5.png'
|
||||
import avatar6 from '@images/avatars/avatar-6.png'
|
||||
|
||||
const props = defineProps({
|
||||
kanbanItem: {
|
||||
type: null,
|
||||
required: false,
|
||||
default: () => ({
|
||||
item: {
|
||||
title: '',
|
||||
dueDate: '2022-01-01T00:00:00Z',
|
||||
labels: [],
|
||||
members: [],
|
||||
id: 0,
|
||||
attachments: 0,
|
||||
commentsCount: 0,
|
||||
image: '',
|
||||
comments: '',
|
||||
},
|
||||
boardId: 0,
|
||||
boardName: '',
|
||||
}),
|
||||
},
|
||||
isDrawerOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:isDrawerOpen',
|
||||
'update:kanbanItem',
|
||||
'deleteKanbanItem',
|
||||
])
|
||||
|
||||
const refEditTaskForm = ref()
|
||||
|
||||
const labelOptions = [
|
||||
'UX',
|
||||
'Image',
|
||||
'Code Review',
|
||||
'Dashboard',
|
||||
'App',
|
||||
'Charts & Maps',
|
||||
]
|
||||
|
||||
const localKanbanItem = ref(JSON.parse(JSON.stringify(props.kanbanItem.item)))
|
||||
|
||||
const handleDrawerModelValueUpdate = val => {
|
||||
emit('update:isDrawerOpen', val)
|
||||
if (!val)
|
||||
refEditTaskForm.value?.reset()
|
||||
}
|
||||
|
||||
// kanban item watcher
|
||||
watch(() => props.kanbanItem, () => {
|
||||
localKanbanItem.value = JSON.parse(JSON.stringify(props.kanbanItem.item))
|
||||
}, { deep: true })
|
||||
|
||||
const updateKanbanItem = () => {
|
||||
refEditTaskForm.value?.validate().then(async valid => {
|
||||
if (valid.valid) {
|
||||
emit('update:kanbanItem', {
|
||||
item: localKanbanItem.value,
|
||||
boardId: props.kanbanItem.boardId,
|
||||
boardName: props.kanbanItem.boardName,
|
||||
})
|
||||
emit('update:isDrawerOpen', false)
|
||||
await nextTick()
|
||||
refEditTaskForm.value?.reset()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// delete kanban item
|
||||
const deleteKanbanItem = () => {
|
||||
emit('deleteKanbanItem', {
|
||||
item: localKanbanItem.value,
|
||||
boardId: props.kanbanItem.boardId,
|
||||
boardName: props.kanbanItem.boardName,
|
||||
})
|
||||
emit('update:isDrawerOpen', false)
|
||||
}
|
||||
|
||||
// 👉 label/chip color
|
||||
const resolveLabelColor = {
|
||||
'UX': 'success',
|
||||
'Image': 'warning',
|
||||
'Code Review': 'error',
|
||||
'Dashboard': 'info',
|
||||
'App': 'secondary',
|
||||
'Charts & Maps': 'primary',
|
||||
}
|
||||
|
||||
const users = [
|
||||
{
|
||||
img: avatar1,
|
||||
name: 'John Doe',
|
||||
},
|
||||
{
|
||||
img: avatar2,
|
||||
name: 'Jane Smith',
|
||||
},
|
||||
{
|
||||
img: avatar3,
|
||||
name: 'Robert Johnson',
|
||||
},
|
||||
{
|
||||
img: avatar4,
|
||||
name: 'Lucy Brown',
|
||||
},
|
||||
{
|
||||
img: avatar5,
|
||||
name: 'Mike White',
|
||||
},
|
||||
{
|
||||
img: avatar6,
|
||||
name: 'Anna Black',
|
||||
},
|
||||
]
|
||||
|
||||
const fileAttached = ref()
|
||||
|
||||
const editor = useEditor({
|
||||
content: '',
|
||||
extensions: [
|
||||
StarterKit,
|
||||
TextAlign.configure({
|
||||
types: [
|
||||
'heading',
|
||||
'paragraph',
|
||||
],
|
||||
}),
|
||||
Placeholder.configure({ placeholder: 'Write a Comment...' }),
|
||||
Underline,
|
||||
],
|
||||
})
|
||||
|
||||
const config = ref({
|
||||
altFormat: 'j M, Y',
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VNavigationDrawer
|
||||
data-allow-mismatch
|
||||
location="end"
|
||||
:width="370"
|
||||
temporary
|
||||
border="0"
|
||||
:model-value="props.isDrawerOpen"
|
||||
@update:model-value="handleDrawerModelValueUpdate"
|
||||
>
|
||||
<!-- 👉 Header -->
|
||||
<AppDrawerHeaderSection
|
||||
title="Edit Task"
|
||||
@cancel="$emit('update:isDrawerOpen', false)"
|
||||
/>
|
||||
|
||||
<VDivider />
|
||||
|
||||
<PerfectScrollbar
|
||||
:options="{ wheelPropagation: false }"
|
||||
style="block-size: calc(100vh - 4rem);"
|
||||
>
|
||||
<VForm
|
||||
v-if="localKanbanItem"
|
||||
ref="refEditTaskForm"
|
||||
@submit.prevent="updateKanbanItem"
|
||||
>
|
||||
<VCardText class="kanban-editor-drawer">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<AppTextField
|
||||
v-model="localKanbanItem.title"
|
||||
label="Title"
|
||||
:rules="[requiredValidator]"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppDateTimePicker
|
||||
v-model="localKanbanItem.dueDate"
|
||||
label="Due date"
|
||||
:config="config"
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<AppSelect
|
||||
v-model="localKanbanItem.labels"
|
||||
:items="labelOptions"
|
||||
label="Label"
|
||||
multiple
|
||||
eager
|
||||
>
|
||||
<template #chip="{ item }">
|
||||
<VChip :color="resolveLabelColor[item.raw]">
|
||||
{{ item.raw }}
|
||||
</VChip>
|
||||
</template>
|
||||
</AppSelect>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<p
|
||||
class="mb-1 text-body-2 text-high-emphasis"
|
||||
style="line-height: 15px;"
|
||||
>
|
||||
Assigned
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<VSelect
|
||||
v-model="localKanbanItem.members"
|
||||
:items="users"
|
||||
item-title="name"
|
||||
item-value="name"
|
||||
multiple
|
||||
return-object
|
||||
variant="plain"
|
||||
:menu-props="{
|
||||
offset: 10,
|
||||
}"
|
||||
class="assignee-select"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<VAvatar size="26">
|
||||
<VImg :src="item.raw.img" />
|
||||
|
||||
<VTooltip activator="parent">
|
||||
{{ item.raw.name }}
|
||||
</VTooltip>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<template #prepend-inner>
|
||||
<IconBtn
|
||||
size="26"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
>
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-plus"
|
||||
/>
|
||||
</IconBtn>
|
||||
</template>
|
||||
</VSelect>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VFileInput
|
||||
v-model="fileAttached"
|
||||
prepend-icon=""
|
||||
multiple
|
||||
variant="outlined"
|
||||
label="No file chosen"
|
||||
clearable
|
||||
>
|
||||
<template #append>
|
||||
<VBtn variant="tonal">
|
||||
Choose
|
||||
</VBtn>
|
||||
</template>
|
||||
</VFileInput>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<p
|
||||
class="text-body-2 text-high-emphasis mb-1"
|
||||
style="line-height: 15px;"
|
||||
>
|
||||
COMMENT
|
||||
</p>
|
||||
<div class="border rounded px-3 py-2">
|
||||
<EditorContent :editor="editor" />
|
||||
<div
|
||||
v-if="editor"
|
||||
class="d-flex justify-end flex-wrap gap-x-2"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-bold"
|
||||
:color="editor.isActive('bold') ? 'primary' : 'secondary'"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('underline') ? 'primary' : 'secondary'"
|
||||
icon="tabler-underline"
|
||||
size="20"
|
||||
@click="editor.commands.toggleUnderline()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive('italic') ? 'primary' : 'secondary'"
|
||||
icon="tabler-italic"
|
||||
size="20"
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive({ textAlign: 'left' }) ? 'primary' : 'secondary'"
|
||||
icon="tabler-align-left"
|
||||
size="20"
|
||||
@click="editor.chain().focus().setTextAlign('left').run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive({ textAlign: 'center' }) ? 'primary' : 'secondary'"
|
||||
icon="tabler-align-center"
|
||||
size="20"
|
||||
@click="editor.chain().focus().setTextAlign('center').run()"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
:color="editor.isActive({ textAlign: 'right' }) ? 'primary' : 'secondary'"
|
||||
icon="tabler-align-right"
|
||||
size="20"
|
||||
@click="editor.chain().focus().setTextAlign('right').run()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
|
||||
<VCol cols="12">
|
||||
<VBtn
|
||||
type="submit"
|
||||
class="me-4"
|
||||
>
|
||||
Update
|
||||
</VBtn>
|
||||
<VBtn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="deleteKanbanItem"
|
||||
>
|
||||
Delete
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VCardText>
|
||||
</VForm>
|
||||
</PerfectScrollbar>
|
||||
</VNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.kanban-editor-drawer {
|
||||
.assignee-select {
|
||||
.v-field__append-inner {
|
||||
.v-select__menu-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 0;
|
||||
min-block-size: 7vh !important;
|
||||
|
||||
p {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
resources/js/views/apps/kanban/KanbanCard.vue
Normal file
171
resources/js/views/apps/kanban/KanbanCard.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
boardId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
boardName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['deleteKanbanItem'])
|
||||
|
||||
const resolveLabelColor = {
|
||||
'UX': 'success',
|
||||
'Image': 'warning',
|
||||
'Code Review': 'error',
|
||||
'Dashboard': 'info',
|
||||
'App': 'secondary',
|
||||
'Charts & Maps': 'primary',
|
||||
}
|
||||
|
||||
const moreOptions = [
|
||||
{
|
||||
title: 'Copy Task link',
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
title: 'Duplicate Task',
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => {
|
||||
emit('deleteKanbanItem', {
|
||||
item: props.item,
|
||||
boardId: props.boardId,
|
||||
boardName: props.boardName,
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
v-if="item"
|
||||
:ripple="false"
|
||||
:link="false"
|
||||
class="kanban-card position-relative"
|
||||
>
|
||||
<VCardText class="d-flex flex-column gap-2">
|
||||
<div class="d-flex align-start gap-2">
|
||||
<div
|
||||
v-if="item.labels && item.labels.length"
|
||||
class="d-flex flex-wrap gap-2"
|
||||
>
|
||||
<VChip
|
||||
v-for="text in item.labels"
|
||||
:key="text"
|
||||
size="small"
|
||||
:color="resolveLabelColor[text]"
|
||||
>
|
||||
{{ text }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<VSpacer />
|
||||
|
||||
<VMenu>
|
||||
<template #activator="{ props: p, isActive }">
|
||||
<VIcon
|
||||
v-bind="p"
|
||||
icon="tabler-dots-vertical"
|
||||
class="position-absolute more-options"
|
||||
style="inset-block-start: 16px; inset-inline-end: 10px;"
|
||||
:style="isActive ? 'opacity: 1' : ''"
|
||||
size="20"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<VList
|
||||
:items="moreOptions"
|
||||
item-props
|
||||
/>
|
||||
</VMenu>
|
||||
</div>
|
||||
|
||||
<!-- Task Img -->
|
||||
<VImg
|
||||
v-if="item.image && item.image.length"
|
||||
:src="item.image"
|
||||
class="rounded"
|
||||
/>
|
||||
|
||||
<!-- Task title -->
|
||||
<p class="text-base text-high-emphasis mb-0">
|
||||
{{ item.title }}
|
||||
</p>
|
||||
|
||||
<!-- footer -->
|
||||
<div class="task-footer d-flex align-center flex-wrap justify-space-between">
|
||||
<div
|
||||
v-if="item.attachments || item.commentsCount"
|
||||
class="d-flex align-center gap-4"
|
||||
>
|
||||
<div v-if="item.attachments">
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-paperclip"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-body-1 d-inline-block">{{ item.attachments }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.commentsCount">
|
||||
<VIcon
|
||||
size="20"
|
||||
icon="tabler-message-2"
|
||||
class="me-1"
|
||||
/>
|
||||
<span class="text-body-1 d-inline-block">{{ item.commentsCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.members && item.members.length"
|
||||
class="v-avatar-group"
|
||||
>
|
||||
<VAvatar
|
||||
v-for="avatar in item.members"
|
||||
:key="avatar.name"
|
||||
size="30"
|
||||
>
|
||||
<VImg :src="avatar.img" />
|
||||
<VTooltip activator="parent">
|
||||
{{ avatar.name }}
|
||||
</VTooltip>
|
||||
</VAvatar>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kanban-card {
|
||||
cursor: grab;
|
||||
|
||||
:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&[style^="z-index"] {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.more-options {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover .more-options {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
312
resources/js/views/apps/kanban/KanbanItems.vue
Normal file
312
resources/js/views/apps/kanban/KanbanItems.vue
Normal file
@@ -0,0 +1,312 @@
|
||||
<script setup>
|
||||
import {
|
||||
animations,
|
||||
handleEnd,
|
||||
performTransfer,
|
||||
} from '@formkit/drag-and-drop'
|
||||
import { dragAndDrop } from '@formkit/drag-and-drop/vue'
|
||||
import { VForm } from 'vuetify/components/VForm'
|
||||
import KanbanCard from '@/views/apps/kanban/KanbanCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
kanbanIds: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
groupName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
boardName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
boardId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
kanbanData: {
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'renameBoard',
|
||||
'deleteBoard',
|
||||
'addNewItem',
|
||||
'editItem',
|
||||
'updateItemsState',
|
||||
'deleteItem',
|
||||
])
|
||||
|
||||
const refKanbanBoard = ref()
|
||||
const localBoardName = ref(props.boardName)
|
||||
const localIds = ref(props.kanbanIds)
|
||||
const isAddNewFormVisible = ref(false)
|
||||
const isBoardNameEditing = ref(false)
|
||||
const refForm = ref()
|
||||
const newTaskTitle = ref('')
|
||||
const refKanbanBoardTitle = ref()
|
||||
|
||||
// 👉 required validator
|
||||
const boardActions = [
|
||||
{
|
||||
title: 'Rename',
|
||||
prependIcon: 'tabler-pencil',
|
||||
onClick: () => {
|
||||
isBoardNameEditing.value = true
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
prependIcon: 'tabler-trash',
|
||||
onClick: () => emit('deleteBoard', props.boardId),
|
||||
},
|
||||
]
|
||||
|
||||
// 👉 emit rename board event
|
||||
const renameBoard = () => {
|
||||
refKanbanBoardTitle.value?.validate().then(valid => {
|
||||
if (valid.valid) {
|
||||
emit('renameBoard', {
|
||||
oldName: props.boardName,
|
||||
newName: localBoardName.value,
|
||||
boardId: props.boardId,
|
||||
})
|
||||
isBoardNameEditing.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 👉 emit add new item event
|
||||
const addNewItem = () => {
|
||||
refForm.value?.validate().then(valid => {
|
||||
if (valid.valid) {
|
||||
emit('addNewItem', {
|
||||
itemTitle: newTaskTitle.value,
|
||||
boardName: props.boardName,
|
||||
boardId: props.boardId,
|
||||
})
|
||||
isAddNewFormVisible.value = false
|
||||
newTaskTitle.value = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 👉 initialize draggable
|
||||
dragAndDrop({
|
||||
parent: refKanbanBoard,
|
||||
values: localIds,
|
||||
group: props.groupName,
|
||||
draggable: child => child.classList.contains('kanban-card'),
|
||||
plugins: [animations()],
|
||||
performTransfer: (state, data) => {
|
||||
performTransfer(state, data)
|
||||
emit('updateItemsState', {
|
||||
boardId: props.boardId,
|
||||
ids: localIds.value,
|
||||
})
|
||||
},
|
||||
handleEnd: data => {
|
||||
handleEnd(data)
|
||||
emit('updateItemsState', {
|
||||
boardId: props.boardId,
|
||||
ids: localIds.value,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// 👉 watch kanbanIds its is useful when you add new task
|
||||
watch(() => props, () => {
|
||||
localIds.value = props.kanbanIds
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
const resolveItemUsingId = id => props.kanbanData.items.find(item => item.id === id)
|
||||
|
||||
const deleteItem = item => {
|
||||
emit('deleteItem', item)
|
||||
}
|
||||
|
||||
// 👉 reset add new item form when esc or close
|
||||
const hideAddNewForm = () => {
|
||||
isAddNewFormVisible.value = false
|
||||
refForm.value?.reset()
|
||||
}
|
||||
|
||||
// close add new item form when you loose focus from the form
|
||||
onClickOutside(refForm, hideAddNewForm)
|
||||
|
||||
// close board name form when you loose focus from the form
|
||||
onClickOutside(refKanbanBoardTitle, () => {
|
||||
isBoardNameEditing.value = false
|
||||
})
|
||||
|
||||
// 👉 reset board rename form when esc or close
|
||||
const hideResetBoardNameForm = () => {
|
||||
isBoardNameEditing.value = false
|
||||
localBoardName.value = props.boardName
|
||||
}
|
||||
|
||||
const handleEnterKeydown = event => {
|
||||
if (event.key === 'Enter' && !event.shiftKey)
|
||||
addNewItem()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="kanban-board">
|
||||
<!-- 👉 board heading and title -->
|
||||
<div class="kanban-board-header pb-4">
|
||||
<VForm
|
||||
v-if="isBoardNameEditing"
|
||||
ref="refKanbanBoardTitle"
|
||||
@submit.prevent="renameBoard"
|
||||
>
|
||||
<VTextField
|
||||
v-model="localBoardName"
|
||||
autofocus
|
||||
variant="underlined"
|
||||
:rules="[requiredValidator]"
|
||||
hide-details
|
||||
class="border-0"
|
||||
@keydown.esc="hideResetBoardNameForm"
|
||||
>
|
||||
<template #append-inner>
|
||||
<VIcon
|
||||
size="20"
|
||||
color="success"
|
||||
icon="tabler-check"
|
||||
class="me-1"
|
||||
@click="renameBoard"
|
||||
/>
|
||||
|
||||
<VIcon
|
||||
size="20"
|
||||
color="error"
|
||||
icon="tabler-x"
|
||||
@click="hideResetBoardNameForm"
|
||||
/>
|
||||
</template>
|
||||
</VTextField>
|
||||
</VForm>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-center justify-space-between "
|
||||
>
|
||||
<h4 class="text-lg font-weight-medium text-truncate">
|
||||
{{ boardName }}
|
||||
</h4>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<VIcon
|
||||
class="drag-handler"
|
||||
size="20"
|
||||
icon="tabler-arrows-move"
|
||||
/>
|
||||
|
||||
<MoreBtn
|
||||
size="28"
|
||||
icon-size="20"
|
||||
class="text-high-emphasis"
|
||||
:menu-list="boardActions"
|
||||
item-props
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 👉 draggable task start here -->
|
||||
<div
|
||||
v-if="localIds"
|
||||
ref="refKanbanBoard"
|
||||
class="kanban-board-drop-zone rounded d-flex flex-column gap-4"
|
||||
:class="localIds.length ? 'mb-4' : ''"
|
||||
>
|
||||
<template
|
||||
v-for="id in localIds"
|
||||
:key="id"
|
||||
>
|
||||
<KanbanCard
|
||||
:item="resolveItemUsingId(id)"
|
||||
:board-id="props.boardId"
|
||||
:board-name="props.boardName"
|
||||
@delete-kanban-item="deleteItem"
|
||||
@click="emit('editItem', { item: resolveItemUsingId(id), boardId: props.boardId, boardName: props.boardName })"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 👉 Add new Form -->
|
||||
<div class="add-new-form">
|
||||
<h6
|
||||
class="text-base font-weight-regular cursor-pointer ms-4"
|
||||
@click="isAddNewFormVisible = !isAddNewFormVisible"
|
||||
>
|
||||
<VIcon
|
||||
size="15"
|
||||
icon="tabler-plus"
|
||||
/> Add New Item
|
||||
</h6>
|
||||
|
||||
<VForm
|
||||
v-if="isAddNewFormVisible"
|
||||
ref="refForm"
|
||||
class="mt-4"
|
||||
validate-on="submit"
|
||||
@submit.prevent="addNewItem"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<VTextarea
|
||||
v-model="newTaskTitle"
|
||||
:rules="[requiredValidator]"
|
||||
placeholder="Add Content"
|
||||
autofocus
|
||||
rows="2"
|
||||
@keydown.enter="handleEnterKeydown"
|
||||
@keydown.esc="hideAddNewForm"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex gap-4 flex-wrap">
|
||||
<VBtn
|
||||
size="small"
|
||||
type="submit"
|
||||
>
|
||||
Add
|
||||
</VBtn>
|
||||
<VBtn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="secondary"
|
||||
@click="hideAddNewForm"
|
||||
>
|
||||
Cancel
|
||||
</VBtn>
|
||||
</div>
|
||||
</VForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.kanban-board-header {
|
||||
.drag-handler {
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.drag-handler {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user