Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 173 additions & 9 deletions src/shared/components/ncTable/partials/TableCellMultiSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,49 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div>
<ul>
<li v-for="v in getObjects()" :key="v.id">
{{ v.label }}<span v-if="v.deleted" :title="t('tables', 'This option is outdated.')">&nbsp;⚠️</span>
</li>
</ul>
<div class="cell-multi-selection">
<div v-if="!isEditing" class="non-edit-mode" @click="handleStartEditing">
<ul>
<li v-for="v in getObjects()" :key="v.id">
{{ v.label }}<span v-if="v.deleted" :title="t('tables', 'This option is outdated.')">&nbsp;⚠️</span>
</li>
</ul>
</div>
<div v-else
ref="editingContainer"
class="edit-mode"
tabindex="0"
@keydown.enter.stop="saveChanges"
@keydown.escape.stop="cancelEdit">
<NcSelect v-model="editValues"
:tag-width="80"
:options="getAllNonDeletedOrSelectedOptions"
:multiple="true"
:aria-label-combobox="t('tables', 'Options')"
:disabled="localLoading || !canEditCell()"
:clearable="true"
style="width: 100%;" />
<div v-if="localLoading" class="loading-indicator">
<div class="icon-loading-small icon-loading-inline" />
</div>
</div>
</div>
</template>

<script>

import { NcSelect } from '@nextcloud/vue'
import { translate as t } from '@nextcloud/l10n'
import cellEditMixin from '../mixins/cellEditMixin.js'

export default {
name: 'TableCellMultiSelection',

components: {
NcSelect,
},

mixins: [cellEditMixin],

props: {
column: {
type: Object,
Expand All @@ -35,20 +62,157 @@ export default {
default: null,
},
},

data() {
return {
localEditValues: [],
isInitialEditClick: false,
}
},

computed: {
getOptions() {
return this.column.selectionOptions || []
},
getAllNonDeletedOrSelectedOptions() {
const options = this.getOptions.filter(item => {
return !item.deleted || this.optionIdIsSelected(item.id)
}) || []

options.forEach(opt => {
if (opt.deleted) {
opt.label += ' ⚠️'
}
})
return options
},
editValues: {
get() {
return this.localEditValues
},
set(newValues) {
this.localEditValues = newValues || []
},
},
},

watch: {
isEditing(isEditing) {
if (isEditing) {
this.initEditValues()
// Add click outside listener after the current event loop
// to avoid the same click that triggered editing from closing the editor
this.$nextTick(() => {
document.addEventListener('click', this.handleClickOutside)
})
} else {
document.removeEventListener('click', this.handleClickOutside)
this.isInitialEditClick = false
}
},
},

methods: {
t,

handleStartEditing(event) {
this.isInitialEditClick = true
this.startEditing()
// Stop the event from propagating to avoid immediate click outside
event.stopPropagation()
},

getObjects() {
return this.column.getObjects(this.value)
},
},

optionIdIsSelected(id) {
// Check if the given id is selected (in the value array)
return this.value && this.value.includes(id)
},

getIdArrayFromObjects(objects) {
const ids = []
objects.forEach(o => {
ids.push(o.id)
})
return ids
},

initEditValues() {
if (this.value !== null) {
this.localEditValues = this.column.getObjects(this.value)
} else {
this.localEditValues = []
}
},
cancelEdit() {
this.isEditing = false
this.localEditValues = []
},

async saveChanges() {
if (this.localLoading) {
return
}

const newValue = this.getIdArrayFromObjects(this.editValues)

const success = await this.updateCellValue(newValue)

if (success) {
// trigger immediate re-render
this.$emit('input', newValue)
this.$emit('update:value', newValue)
this.isEditing = false
} else {
this.cancelEdit()
}

this.localLoading = false
},

handleClickOutside(event) {
// Ignore the initial click that started editing
if (this.isInitialEditClick) {
this.isInitialEditClick = false
return
}

// Check if the click is outside the editing container
// But ignore clicks on dropdown options and scrollbars
if (this.$refs.editingContainer && !this.$refs.editingContainer.contains(event.target)) {
this.saveChanges()
}
},
},
}
</script>
<style lang="scss" scoped>
.cell-multi-selection {
width: 100%;

.non-edit-mode {
cursor: pointer;
min-height: 20px;
}
}

.edit-mode {
.editor-buttons {
display: flex;
gap: 8px;
margin-top: 8px;
align-items: center;
}

.icon-loading-inline {
margin-left: 4px;
}
}

ul {
list-style-type: disc;
padding-left: calc(var(--default-grid-baseline) * 3);
}

</style>
136 changes: 131 additions & 5 deletions src/shared/components/ncTable/partials/TableCellSelection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,42 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div>
{{ column.getLabel(value) }}<span v-if="isDeleted()" :title="t('tables', 'This option is outdated.')">&nbsp;⚠️</span>
<div class="cell-selection">
<div v-if="!isEditing" class="non-edit-mode" @click="handleStartEditing">
{{ column.getLabel(value) }}<span v-if="isDeleted()" :title="t('tables', 'This option is outdated.')">&nbsp;⚠️</span>
</div>
<div v-else
ref="editingContainer"
class="edit-mode"
tabindex="0"
@keydown.enter.stop="saveChanges"
@keydown.escape.stop="cancelEdit">
<NcSelect v-model="editValue"
:options="getAllNonDeletedOptions"
:aria-label-combobox="t('tables', 'Options')"
:disabled="localLoading || !canEditCell()"
style="width: 100%;" />
<div v-if="localLoading" class="loading-indicator">
<div class="icon-loading-small icon-loading-inline" />
</div>
</div>
</div>
</template>

<script>

import { NcSelect } from '@nextcloud/vue'
import { translate as t } from '@nextcloud/l10n'
import cellEditMixin from '../mixins/cellEditMixin.js'

export default {
name: 'TableCellSelection',

components: {
NcSelect,
},

mixins: [cellEditMixin],

props: {
column: {
type: Object,
Expand All @@ -31,19 +55,121 @@ export default {
default: null,
},
},

data() {
return {
isInitialEditClick: false,
}
},

computed: {
getOptions() {
return this.column?.selectionOptions || []
},
getAllNonDeletedOptions() {
return this.getOptions.filter(item => {
return !item.deleted
})
},
},

watch: {
isEditing(isEditing) {
if (isEditing) {
this.initEditValue()
// Add click outside listener after the current event loop
// to avoid the same click that triggered editing from closing the editor
this.$nextTick(() => {
document.addEventListener('click', this.handleClickOutside)
})
} else {
// Remove click outside listener
document.removeEventListener('click', this.handleClickOutside)
this.isInitialEditClick = false
}
},
},

methods: {
t,

handleStartEditing(event) {
this.isInitialEditClick = true
this.startEditing()
// Stop the event from propagating to avoid immediate click outside
event.stopPropagation()
},

isDeleted() {
this.column.isDeletedLabel(this.value)
return this.column.isDeletedLabel(this.value)
},

getOptionObject(id) {
return this.getOptions.find(e => e.id === id) || null
},

initEditValue() {
if (this.value !== null) {
this.editValue = this.getOptionObject(parseInt(this.value))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is parseInt here necessary if value is already a Number (in props)?

} else {
this.editValue = null
}
},
async saveChanges() {
if (this.localLoading) {
return
}

const newValue = this.editValue?.id

const success = await this.updateCellValue(newValue)

if (!success) {
this.cancelEdit()
}

this.localLoading = false
this.isEditing = false
},

handleClickOutside(event) {
// Ignore the initial click that started editing
if (this.isInitialEditClick) {
this.isInitialEditClick = false
return
}

// Check if the click is outside the editing container
if (this.$refs.editingContainer && !this.$refs.editingContainer.contains(event.target)) {
this.saveChanges()
}
},
},
}
</script>

<style lang="scss" scoped>
.cell-selection {
width: 100%;

.non-edit-mode {
cursor: pointer;
min-height: 20px;
}
}

:deep(.vs__dropdown-toggle) {
border: var(--vs-border-width) var(--vs-border-style) var(--vs-border-color);
border-radius: var(--vs-border-radius);
}

.edit-mode {
.icon-loading-inline {
margin-left: 4px;
}
}

span {
cursor: help;
}

</style>
2 changes: 0 additions & 2 deletions src/shared/components/ncTable/partials/TableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,9 @@ export default {
nonInlineEditableColumnTypes() {
return [
ColumnTypes.TextRich,
ColumnTypes.SelectionMulti,
ColumnTypes.Datetime,
ColumnTypes.DatetimeDate,
ColumnTypes.DatetimeTime,
ColumnTypes.Selection,
]
},
},
Expand Down
Loading
Loading