Skip to content

Commit

Permalink
Create checklist contentnode
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelmeister committed Jul 5, 2024
1 parent 2f7d862 commit 926aba3
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 25 deletions.
4 changes: 4 additions & 0 deletions common/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
}
},
"contentNode": {
"checklist": {
"icon": "mdi-clipboard-list-outline",
"name": "Checkliste"
},
"columnLayout": {
"entity": {
"column": {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/activity/ContentNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import LearningTopics from './content/LearningTopics.vue'
import SafetyConcept from './content/SafetyConcept.vue'
import Storyboard from './content/Storyboard.vue'
import Storycontext from './content/Storycontext.vue'
import Checklist from './content/Checklist.vue'
const contentNodeComponents = {
ColumnLayout,
Expand All @@ -35,6 +36,7 @@ const contentNodeComponents = {
SafetyConcept,
Storyboard,
Storycontext,
Checklist,
}
export default {
Expand Down
219 changes: 219 additions & 0 deletions frontend/src/components/activity/content/Checklist.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<template>
<ContentNodeCard class="ec-checklist-node" v-bind="$props">
<template #outer>
<DetailPane
v-model="showDialog"
icon="mdi-clipboard-list-outline"
:title="$tc('components.activity.content.checklist.title')"
>
<template #activator="{ on }">
<button
class="text-left"
:class="{ 'theme--light v-input--is-disabled': layoutMode }"
:disabled="layoutMode"
v-on="on"
>
<v-skeleton-loader
v-if="activeChecklists.length === 0"
class="px-4 pb-4"
type="paragraph"
/>
<div
v-for="{ checklist, items } in activeChecklists"
:key="checklist._meta.self"
class="mb-3"
>
<h3 class="px-4">{{ checklist.name }}</h3>
<v-list-item
v-for="{ item, parents } in items"
:key="item._meta.self"
class="min-h-0"
:disabled="layoutMode"
>
<v-list-item-content class="py-2">
<v-list-item-subtitle v-if="parents.length > 0" class="d-flex gap-1">
<template v-for="(parent, index) in parents">
<span v-if="index" :key="parent._meta.self + 'divider'">/</span>
<span
:key="parent._meta.self"

Check warning on line 38 in frontend/src/components/activity/content/Checklist.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

`<template v-for>` key should be placed on the `<template>` tag
class="e-checklist-item-parent-name"
>{{ parent.text }}</span
>
</template>
</v-list-item-subtitle>
<v-list-item-title>
{{ item.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</div>
</button>
</template>
<div
v-for="checklist in camp.checklists().items"
:key="checklist._meta.self"
class="mb-4"
>
<h3 class="mb-1">{{ checklist.name }}</h3>
<ol>
<ChecklistItem
v-for="item in checklist
.checklistItems()
.items.filter(({ parent }) => parent == null)"
:key="item._meta.self"
:checklist="checklist"
:item="item"
@remove-item="removeItem"
@add-item="addItem"
/>
</ol>
</div>
</DetailPane>
</template>
</ContentNodeCard>
</template>

<script>
import ContentNodeCard from '@/components/activity/content/layout/ContentNodeCard.vue'
import { contentNodeMixin } from '@/mixins/contentNodeMixin.js'
import DetailPane from '@/components/generic/DetailPane.vue'
import ChecklistItem from './checklist/ChecklistItem.vue'
import { serverErrorToString } from '@/helpers/serverError.js'
import { debounce, isEqual, sortBy, uniq } from 'lodash'
import { computed } from 'vue'
export default {
name: 'Checklist',
components: { DetailPane, ContentNodeCard, ChecklistItem },
mixins: [contentNodeMixin],
provide() {
return {
checkedItems: computed(() => this.checkedItems),
}
},
data() {
return {
savingRequestCount: 0,
dirty: false,
showDialog: false,
checkedItems: [],
uncheckedItems: [],
errorMessages: null,
debouncedSave: () => null,
itemsLoaded: false,
}
},
computed: {
selectionContentNode() {
return this.api
.get()
.checklistItems()
.items.filter((item) =>
this.contentNode
.checklistItems()
.items.some(({ _meta }) => _meta.self === item._meta.self)
)
},
serverSelection() {
return this.selectionContentNode.map((item) => item.id)
},
activeChecklists() {
return this.camp
.checklists()
.items.filter(({ _meta }) =>
this.contentNode
.checklistItems()
.items.some((item) => _meta.self === item?.checklist()._meta.self)
)
.map((checklist) => ({
checklist,
items: this.selectionContentNode
.filter((item) => item.checklist()._meta.self === checklist._meta.self)
.map((item) => ({
item,
parents: this.itemsLoaded ? this.getParents(item) : [],
})),
}))
},
},
watch: {
serverSelection: {
async handler(newOptions, oldOptions) {
if (isEqual(sortBy(newOptions), sortBy(oldOptions))) {
return
}
// copy incoming data if not dirty or if incoming data is the same as local data
if (!this.dirty || isEqual(sortBy(newOptions), sortBy(this.checkedItems))) {
this.resetLocalData()
}
},
immediate: true,
},
},
created() {
const DEBOUNCE_WAIT = 500
this.debounceSave = debounce(this.save, DEBOUNCE_WAIT)
this.api
.get()
.checklistItems()
._meta.load.then(() => {
this.itemsLoaded = true
})
},
beforeDestroy() {
this.checkedItems = null
this.uncheckedItems = null
},
methods: {
// get all parents of a given item
getParents(item) {
const parents = []
let parent = item?.parent?.()
while (parent) {
parents.unshift(parent)
parent = parent?.parent?.()
}
return parents
},
onInput() {
this.dirty = true
this.errorMessages = []
this.debounceSave()
},
removeItem(item) {
this.uncheckedItems.push(item)
this.checkedItems = this.checkedItems.filter((i) => i !== item)
this.onInput()
},
addItem(item) {
this.checkedItems.push(item)
this.onInput()
},
save() {
this.savingRequestCount++
this.contentNode
.$patch({
addChecklistItemIds: uniq(this.checkedItems),
removeChecklistItemIds: uniq(this.uncheckedItems),
})
.catch((e) => this.errorMessages.push(serverErrorToString(e)))
.finally(() => this.savingRequestCount--)
},
resetLocalData() {
this.checkedItems = [...this.serverSelection]
this.uncheckedItems = []
this.dirty = false
},
},
}
</script>
<style scoped>
.e-checklist-item-parent-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<li>
<div
class="d-flex gap-2 align-baseline pb-2"
@click="checked ? $emit('remove-item', item.id) : $emit('add-item', item.id)"
>
<component :is="checked ? 'strong' : 'span'" class="flex-grow-1"
>{{ item.text }}
</component>
<v-switch inset dense :input-value="checked" hide-details class="mt-0" />
</div>
<ol v-if="children.length > 0">
<ChecklistItem
v-for="child in children"
:key="child._meta.self"
:checklist="checklist"
:item="child"
v-on="$listeners"

Check warning on line 18 in frontend/src/components/activity/content/checklist/ChecklistItem.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

The `$listeners` is deprecated
/>
</ol>
</li>
</template>

<script>
import { filter, sortBy } from 'lodash'
export default {
name: 'ChecklistItem',
inject: ['checkedItems'],
props: {
checklist: {
type: Object,
default: null,
required: false,
},
item: {
type: Object,
default: null,
required: false,
},
},
emits: ['add-item', 'remove-item'],
computed: {
children() {
return sortBy(
filter(
this.checklist.checklistItems().items,
({ parent }) => parent?.()._meta.self === this.item?._meta.self
),
'position'
)
},
checked() {
return this.checkedItems.includes(this.item.id)
},
},
}
</script>
<style scoped></style>
21 changes: 11 additions & 10 deletions frontend/src/components/checklist/SortableChecklist.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<draggable
class="e-checklist-dragarea--inner"
:list="sortedItems"
ghost-class="e-checklist-item--ghost"
ghost-class="e-sortable-checklist-item--ghost"
handle=".drag-and-drop-handle"
filter=".add-item"
:disabled="disabled"
Expand All @@ -13,7 +13,7 @@
@start="dragStart"
@end="dragStop"
>
<ChecklistItem
<SortableChecklistItem
v-for="(item, i) in localSortedItems"
:key="item._meta.self"
:data-href="item._meta.self"
Expand All @@ -32,7 +32,7 @@
>
<template #activator="{ on }">
<v-list-item
class="e-checklist-item__add ml-10 mr-2 my-n1 px-0 rounded-pill min-h-0"
class="e-sortable-checklist-item__add ml-10 mr-2 my-n1 px-0 rounded-pill min-h-0"
v-on="on"
>
<v-avatar class="mr-2" size="32"
Expand All @@ -56,14 +56,14 @@ import { computed } from 'vue'
import draggable from 'vuedraggable'
import { every, sortBy, filter } from 'lodash'
import { errorToMultiLineToast } from '@/components/toast/toasts.js'
import ChecklistItem from '@/components/checklist/ChecklistItem.vue'
import SortableChecklistItem from '@/components/checklist/SortableChecklistItem.vue'
import ChecklistItemCreate from '@/components/checklist/ChecklistItemCreate.vue'
export default {
name: 'SortableChecklist',
components: {
ChecklistItemCreate,
ChecklistItem,
SortableChecklistItem,
draggable,
},
inject: {
Expand Down Expand Up @@ -168,7 +168,7 @@ export default {
</script>
<style scoped>
.e-checklist-item--ghost {
.e-sortable-checklist-item--ghost {
opacity: 0.5;
background: rgb(196, 196, 196);
filter: saturate(0);
Expand All @@ -185,17 +185,18 @@ export default {
background: rgba(0, 130, 236, 0.15);
padding-bottom: 0;
}
.e-checklist-item__add {
.e-sortable-checklist-item__add {
padding-top: 2px;
padding-bottom: 2px;
}
.e-checklist-item__add:not(:hover):not(:focus-within) {
.e-sortable-checklist-item__add:not(:hover):not(:focus-within) {
opacity: 0.6;
}
.e-checklist-item__add:is(:hover, :focus-within) {
.e-sortable-checklist-item__add:is(:hover, :focus-within) {
color: #1976d2 !important;
}
.e-checklist-dragarea:not(:hover):not(:focus-within) :deep(.e-checklist-item__add) {
.e-checklist-dragarea:not(:hover):not(:focus-within)
:deep(.e-sortable-checklist-item__add) {
display: none;
}
</style>
Loading

0 comments on commit 926aba3

Please sign in to comment.