WIP: feature/single-task-in-list-reworked #3180

Closed
dpschen wants to merge 3 commits from dpschen/frontend:feature/single-task-in-list-reworked into main
7 changed files with 541 additions and 400 deletions

View File

@ -0,0 +1,164 @@
<template>
<progress
class="progress-bar"
:class="{
'is-small': isSmall,
'is-primary': isPrimary,
}"
:value="value"
max="100"
>
{{ value }}%
</progress>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
value: {
type: Number,
required: true,
},
isSmall: {
type: Boolean,
default: false,
},
isPrimary: {
type: Boolean,
required: false,
},
})
</script>
<style lang="scss" scoped>
.progress-bar {
--progress-height: var(--size-normal, #{$size-normal});
--progress-bar-background-color: var(--border-light, #{$border-light});
--progress-value-background-color: var(--text, #{$text});
--progress-border-radius: var(--radius-rounded, #{$radius-rounded});
--progress-indeterminate-duration: 1.5s;
--size-small: #{$size-small};
--size-medium: #{$size-medium};
--size-large: #{$size-large};
appearance: none;
border: none;
border-radius: var(--progress-border-radius);
display: block;
height: var(--progress-height);
overflow: hidden;
padding: 0;
width: 100%;
&::-webkit-progress-bar {
background-color: var(--progress-bar-background-color);
}
&::-webkit-progress-value {
background-color: var(--progress-value-background-color);
}
&::-moz-progress-bar {
background-color: var(--progress-value-background-color);
}
&::-ms-fill {
background-color: var(--progress-value-background-color);
border: none;
}
// Colors
@each $name, $pair in $colors {
$color: nth($pair, 1);
&.is-#{$name} {
&::-webkit-progress-value {
--progress-value-background-color: var(--#{$name}, #{$color});
}
&::-moz-progress-bar {
--progress-value-background-color: var(--#{$name}, #{$color});
}
&::-ms-fill {
--progress-value-background-color: var(--#{$name}, #{$color});
}
&:indeterminate {
background-image: linear-gradient(
to right,
var(--#{$name}, #{$color}) 30%,
var(--progress-bar-background-color) 30%
);
}
}
}
&:indeterminate {
animation-duration: var(--progress-indeterminate-duration);
animation-iteration-count: infinite;
animation-name: moveIndeterminate;
animation-timing-function: linear;
background-color: var(--progress-bar-background-color);
background-image: linear-gradient(
to right,
var(--text, #{$text}) 30%,
var(--progress-bar-background-color) 30%
);
background-position: top left;
background-repeat: no-repeat;
background-size: 150% 150%;
&::-webkit-progress-bar {
background-color: transparent;
}
&::-moz-progress-bar {
background-color: transparent;
}
&::-ms-fill {
animation-name: none;
}
}
// Sizes
&.is-small {
--progress-height: var(--size-small, #{$size-small});
}
&.is-medium {
--progress-height: var(--size-medium, #{$size-medium});
}
&.is-large {
--progress-height: var(--size-large, #{$size-large});
}
}
@keyframes moveIndeterminate {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.progress-bar {
--progress-height: var(--size-normal, 1rem);
border-radius: $radius-large;
width: inherit; // overwrite bulma
min-width: 6vw;
@media screen and (max-width: $tablet) {
width: 100%;
}
&::-moz-progress-bar,
&::-webkit-progress-value {
background: var(--grey-500);
}
}
.progress-bar.is-small {
--progress-height: var(--size-small, 0.75rem);
}
</style>

View File

@ -16,14 +16,11 @@
type="file"
v-if="editEnabled"
/>
<progress
:value="attachmentService.uploadProgress"
class="progress is-primary"
max="100"
<ProgressBar
v-if="attachmentService.uploadProgress > 0"
>
{{ attachmentService.uploadProgress }}%
</progress>
:value="attachmentService.uploadProgress * 100"
is-primary
/>
<div class="files" v-if="attachments.length > 0">
<a
@ -34,7 +31,7 @@
>
<div class="filename">{{ a.file.name }}</div>
<div class="info">
<p class="attachment-info-meta">
<p class="collapses">
<i18n-t keypath="task.attachment.createdBy">
<span v-tooltip="formatDate(a.created)">
{{ formatDateSince(a.created) }}
@ -83,7 +80,7 @@
@click="$refs.files.click()"
class="mb-4"
icon="cloud-upload-alt"
variant="secondary"
type="secondary"
:shadow="false"
>
{{ $t('task.attachment.upload') }}
@ -138,7 +135,8 @@
<script>
import AttachmentService from '../../../services/attachment'
import AttachmentModel from '../../../models/attachment'
import User from '../../misc/user'
import User from '@/components/misc/user'
import ProgressBar from '@/components/misc/ProgressBar'
import {mapState} from 'vuex'
import copy from 'copy-to-clipboard'
@ -148,6 +146,7 @@ export default {
name: 'attachments',
components: {
User,
ProgressBar,
},
data() {
return {
@ -289,6 +288,21 @@ export default {
content: '·';
padding: 0 .25rem;
}
@media screen and (max-width: $mobile) {
&.collapses {
flex-direction: column;
> span:not(:last-child):after,
> a:not(:last-child):after {
display: none;
}
.user .username {
display: none;
}
}
}
}
}
}
@ -326,10 +340,6 @@ export default {
height: auto;
text-shadow: var(--shadow-md);
animation: bounce 2s infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
.hint {
@ -346,35 +356,6 @@ export default {
}
}
.attachment-info-meta {
display: flex;
align-items: center;
:deep(.user) {
display: flex !important;
align-items: center;
margin: 0 .5rem;
}
@media screen and (max-width: $mobile) {
flex-direction: column;
align-items: flex-start;
:deep(.user) {
margin: .5rem 0;
}
> span:not(:last-child):after,
> a:not(:last-child):after {
display: none;
}
.user .username {
display: none;
}
}
}
@keyframes bounce {
from,
20%,
@ -400,6 +381,4 @@ export default {
transform: translate3d(0, -4px, 0);
}
}
@include modal-transition();
</style>

View File

@ -28,17 +28,16 @@
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
<time :datetime="formatISO(task.dueDate)">
<span>
{{ formatDateSince(task.dueDate) }}
</time>
</span>
</span>
<h3>{{ task.title }}</h3>
<progress
class="progress is-small"
<ProgressBar
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
:value="task.percentDone * 100"
is-small
/>
<div class="footer">
<labels :labels="task.labels"/>
<priority-label :priority="task.priority" :done="task.done"/>
@ -67,20 +66,20 @@
<script>
import {playPop} from '../../../helpers/playPop'
import PriorityLabel from '../../../components/tasks/partials/priorityLabel'
import User from '../../../components/misc/user'
import PriorityLabel from '@/components/tasks/partials/priorityLabel'
import User from '@/components/misc/user'
import ProgressBar from '@/components/misc/ProgressBar'
import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels'
import Labels from '@/components/tasks/partials/labels'
import ChecklistSummary from './checklist-summary'
import {colorIsDark} from '@/helpers/color/colorIsDark'
export default {
name: 'kanban-card',
components: {
ChecklistSummary,
Done,
PriorityLabel,
ProgressBar,
User,
Labels,
},
@ -100,7 +99,6 @@ export default {
},
},
methods: {
colorIsDark,
async toggleTaskDone(task) {
this.loadingInternal = true
try {
@ -138,6 +136,7 @@ $task-background: var(--white);
border: 3px solid transparent;
font-size: .9rem;
margin: .5rem;
padding: .4rem;
border-radius: $radius;
background: $task-background;

View File

@ -1,365 +1,378 @@
<template>
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
<fancycheckbox :disabled="isArchived || disabled" @change="markAsDone" v-model="task.done"/>
<div
class="task loader-container"
:class="{'is-loading': taskService.loading}"
@click="$router.push({ name: taskDetailRoute, params: { id: task.id } })"
>
<fancycheckbox
class="fancycheckbox"
:disabled="isArchived || disabled" @change="markAsDone"
v-model="task.done"
/>
<span
v-if="showListColor && listColor !== ''"
:style="{backgroundColor: listColor }"
class="color-bubble"
>
</span>
<router-link
:to="taskDetailRoute"
:class="{ 'done': task.done}"
class="tasktext">
<span>
<router-link
:to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list"
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }}
</router-link>
:style="{backgroundColor: listColor }"
/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template>
>
</span>
{{ task.title }}
</span>
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0"/>
<user
:avatar-size="27"
:is-inline="true"
:key="task.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
v-for="(a, i) in task.assignees"
/>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
@click.prevent.stop="showDefer = !showDefer"
v-if="+new Date(task.dueDate) > 0"
v-tooltip="formatDate(task.dueDate)"
:aria-expanded="showDefer ? 'true' : 'false'"
>
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
<transition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</transition>
<priority-label :priority="task.priority" :done="task.done"/>
<span>
<span class="list-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span class="list-task-icon" v-if="task.description">
<icon icon="align-left"/>
</span>
<span class="list-task-icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</span>
<checklist-summary :task="task"/>
</router-link>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
<router-link
:to="{ name: 'list.list', params: { listId: task.listId } }"
v-if="showList && list !== null"
class="task-list"
v-if="!showList && currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }}
:to="{ name: 'list.list', params: { listId: task.listId } }"
v-tooltip="$t('task.detail.belongsToList', {list: list.title})"
>
{{ list.title }}
</router-link>
<router-link
class="tasktext"
:to="{ name: taskDetailRoute, params: { id: task.id } }"
:class="{ 'done': task.done }"
>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span
v-if="task.relatedTasks.parenttask"
class="parent-tasks"
>
<template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template>
>
</span>
<span class="task-title">{{ task.title }}</span>
</router-link>
<labels class="labels" :labels="task.labels" v-if="task.labels.length > 0"/>
<user
v-for="(a, i) in task.assignees"
:key="task.id + 'assignee' + a.id + i"
:avatar-size="27"
:show-username="false"
:user="a"
/>
<popup v-if="+new Date(task.dueDate) > 0">
<template #trigger="{toggle}">
<em
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
@click.prevent.stop="toggle()"
v-tooltip="formatDate(task.dueDate)"
>
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</em>
</template>
<template #content="{isOpen}">
<transition name="fade">
<defer-task v-if="isOpen" v-model="task" />
</transition>
</template>
</popup>
<priority-label :priority="task.priority" :done="task.done"/>
<icon
v-if="task.attachments.length > 0"
class="list-task-icon"
icon="paperclip"
/>
<icon
v-if="task.description"
class="list-task-icon"
icon="align-left"
/>
<icon
v-if="task.repeatAfter.amount > 0"
class="list-task-icon"
icon="history"
/>
<checklist-summary :task="task"/>
<ProgressBar
v-if="task.percentDone > 0"
:value="task.percentDone * 100"
is-small
/>
<router-link
v-if="!showList && currentList.id !== task.listId && list !== null"
class="task-list"
:to="{ name: 'list.list', params: { listId: task.listId } }"
v-tooltip="$t('task.detail.belongsToList', {list: list.title})"
>
{{ list.title }}
</router-link>
<a
class="favorite"
:class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite"
class="favorite">
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
>
<icon :icon="task.isFavorite ? 'star' : ['far', 'star']" />
</a>
<slot></slot>
</div>
</template>
<script>
import TaskModel from '../../../models/task'
<script lang="ts" setup>
import {reactive, computed, watch, defineProps, defineEmits, PropType } from 'vue'
import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex'
import TaskModel from '@/models/task'
import TaskService from '@/services/task'
import PriorityLabel from './priorityLabel'
import TaskService from '../../../services/task'
import Labels from './labels'
import User from '../../misc/user'
import Fancycheckbox from '../../input/fancycheckbox'
import DeferTask from './defer-task'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {playPop} from '@/helpers/playPop'
import Labels from './labels'
import ChecklistSummary from './checklist-summary'
export default {
name: 'singleTaskInList',
data() {
return {
taskService: new TaskService(),
task: new TaskModel(),
showDefer: false,
import User from '@/components/misc/user'
import Fancycheckbox from '@/components/input/fancycheckbox'
import Popup from '@/components/misc/popup.vue'
import ProgressBar from '@/components/misc/ProgressBar'
import {playPop} from '@/helpers/playPop'
import {success} from '@/message'
const props = defineProps({
theTask: {
type: Object as PropType<TaskModel>,
required: true,
},
isArchived: {
type: Boolean,
default: false,
},
taskDetailRoute: {
type: String,
default: 'task.list.detail',
},
showList: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
showListColor: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['task-updated'])
const taskService = reactive(new TaskService())
const task = reactive(new TaskModel())
watch(props.theTask, (newVal) => {
Object.assign(task, newVal)
}, { immediate: true },
)
const store = useStore()
const currentList = computed(() => {
return typeof store.state.currentList === 'undefined' ? {
id: 0,
title: '',
} : store.state.currentList
})
const list = computed(() => store.getters['lists/getListById'](task.listId))
const listColor = computed(() => list.value?.hexColor || '')
const { t } = useI18n()
async function markAsDone(checked: boolean) {
async function updateFunc() {
const newTask = await taskService.update(task)
if (task.done) {
playPop()
}
},
components: {
ChecklistSummary,
DeferTask,
Fancycheckbox,
User,
Labels,
PriorityLabel,
},
props: {
theTask: {
type: TaskModel,
required: true,
},
isArchived: {
type: Boolean,
default: false,
},
showList: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
showListColor: {
type: Boolean,
default: true,
},
},
emits: ['task-updated'],
watch: {
theTask(newVal) {
this.task = newVal
},
},
mounted() {
this.task = this.theTask
document.addEventListener('click', this.hideDeferDueDatePopup)
},
beforeUnmount() {
document.removeEventListener('click', this.hideDeferDueDatePopup)
},
computed: {
listColor() {
const list = this.$store.getters['lists/getListById'](this.task.listId)
return list !== null ? list.hexColor : ''
},
currentList() {
return typeof this.$store.state.currentList === 'undefined' ? {
id: 0,
title: '',
} : this.$store.state.currentList
},
taskDetailRoute() {
return {
name: 'task.detail',
params: { id: this.task.id },
// TODO: re-enable opening task detail in modal
// state: { backdropView: this.$router.currentRoute.value.fullPath },
}
},
},
methods: {
async markAsDone(checked) {
const updateFunc = async () => {
const task = await this.taskService.update(this.task)
if (this.task.done) {
playPop()
}
this.task = task
this.$emit('task-updated', task)
this.$message.success({
message: this.task.done ?
this.$t('task.doneSuccess') :
this.$t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback() {
this.task.done = !this.task.done
this.markAsDone(!checked)
},
}])
}
Object.assign(task, newTask)
emit('task-updated', task)
success({
message: task.done ?
t('task.doneSuccess') :
t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback() {
task.done = !task.done
markAsDone(!checked)
},
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
},
if (checked) {
// Delay it to show the animation when marking a task as done
setTimeout(updateFunc, 300)
} else {
// Don't delay it when un-marking it as it doesn't have an animation the other way around
await updateFunc()
}
}
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task)
this.$emit('task-updated', this.task)
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
},
hideDeferDueDatePopup(e) {
if (!this.showDefer) {
return
}
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
this.showDefer = false
})
},
},
async function toggleFavorite() {
task.isFavorite = !task.isFavorite
const newTask = await taskService.update(task)
Object.assign(task, newTask)
emit('task-updated', task)
store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
}
</script>
<style lang="scss" scoped>
$line-height: 2.5;
.task {
display: flex;
flex-wrap: wrap;
padding: .4rem;
display: grid;
grid-template-areas:
"checkbox color-bubble list1 text list2";
grid-template-columns: min-content max-content fit-content(10ch) minmax(0, 3fr) fit-content(10ch);
grid-auto-flow: column;
grid-auto-columns: min-content;
grid-template-rows: 1fr;
place-items: center start;
transition: background-color $transition;
align-items: center;
cursor: pointer;
border-radius: $radius;
border: 2px solid transparent;
line-height: $line-height;
&:hover {
background-color: var(--grey-100);
}
.tasktext,
&.tasktext {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
flex: 1 0 50%;
.overdue {
color: var(--danger);
}
> * {
min-width: min-content;
}
.task-list {
width: auto;
color: var(--grey-400);
font-size: .9rem;
white-space: nowrap;
}
.color-bubble {
height: 10px;
flex: 0 0 10px;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: 5px;
height: 27px;
width: 27px;
}
.list-task-icon {
margin-left: 6px;
&:not(:first-of-type) {
margin-left: 8px;
}
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.favorite {
opacity: 0;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
&:hover .favorite {
opacity: 1;
}
.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
&:hover .handle {
opacity: 1;
}
:deep(.fancycheckbox) {
height: 18px;
padding-top: 0;
padding-right: .5rem;
span {
display: none;
}
}
.tasktext.done {
text-decoration: line-through;
color: var(--grey-500);
}
span.parent-tasks {
color: var(--grey-500);
width: auto;
}
.remove {
color: var(--danger);
}
input[type="checkbox"] {
vertical-align: middle;
}
.settings {
float: right;
width: 24px;
cursor: pointer;
}
&.loader-container.is-loading:after {
top: calc(50% - 1rem);
left: calc(50% - 1rem);
width: 2rem;
height: 2rem;
border-left-color: var(--grey-300);
border-bottom-color: var(--grey-300);
> * + * {
// gap does not work since we have collapsed tracks
margin-left: 0.5rem;
}
}
.task.loader-container.is-loading::after {
top: 50%;
left: 50%;
width: 2rem;
height: 2rem;
transform: translate(-50%, -50%);
border-left-color: var(--grey-300);
border-bottom-color: var(--grey-300);
}
.fancycheckbox {
grid-area: checkbox;
align-self: stretch;
padding-left: .25rem;
padding-right: .5rem;
margin-right: -0.5rem;
}
.color-bubble {
grid-area: color-bubble;
width: 10px;
height: 10px;
}
@mixin overflow-text() {
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.task-list {
grid-area: list1;
max-width: 100%;
min-width: 0; // this is needed for the overflow text
@include overflow-text();
font-size: .9rem;
color: var(--grey-400);
}
.tasktext {
grid-area: text;
max-width: 100%;
min-width: 0; // this is needed for the overflow text
display: flex;
justify-content: space-between;
align-items: center;
}
.tasktext.done {
text-decoration: line-through;
color: var(--grey-500);
}
.parent-tasks {
color: var(--grey-500);
width: auto;
}
.task-title {
flex: 1 1 0;
@include overflow-text();
min-width: 6vw;
}
.overdue {
color: var(--danger);
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
height: 27px;
width: 27px;
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.favorite {
opacity: 0;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
.task:hover .favorite {
opacity: 1;
}
:deep(.handle) {
opacity: 0;
transition: opacity $transition;
cursor: grab;
}
.task:hover :deep(.handle) {
opacity: 1;
}
</style>

View File

@ -15,11 +15,6 @@
}
}
.progress {
// overwrite bulma
margin-bottom: 0;
}
&.noborder {
margin: 1rem -0.5rem;
}

View File

@ -24,7 +24,7 @@
@import "bulma-css-variables/sass/elements/icon";
@import "bulma-css-variables/sass/elements/image";
//@import "bulma-css-variables/sass/elements/notification"; // not used
@import "bulma-css-variables/sass/elements/progress";
// @import "bulma-css-variables/sass/elements/progress"; // not used
@import "bulma-css-variables/sass/elements/table";
@import "bulma-css-variables/sass/elements/tag";
@import "bulma-css-variables/sass/elements/title";

View File

@ -38,20 +38,12 @@ h6 {
// - kanban-card.vue
// - singleTaskInList.vue
.progress {
border-radius: $radius-large;
width: 50px;
margin: 0 0.5rem 0 0;
flex: 3 1 auto;
&::-moz-progress-bar,
&::-webkit-progress-value {
background: var(--grey-500);
}
// margin: 0 0.5rem 0 0;
// flex: 3 1 auto;
@media screen and (max-width: $tablet) {
margin: 0.5rem 0 0 0;
order: 1;
width: 100%;
// margin: 0.5rem 0 0 0;
// order: 1;
}
}
@ -136,7 +128,6 @@ button.table {
.color-bubble {
display: inline-block;
border-radius: 100%;
margin-right: 4px;
}
.dropdown-menu .dropdown-content {