Compare commits

...

6 Commits

Author SHA1 Message Date
profi248 0a5c0869cf Make adding fields to tasks more intuitive
- briefly flash the background of (most) fields that have just been added
- scroll the newly added field into viewport if not visible already
2021-01-02 13:36:49 +01:00
azymondrian 158e697988 Show task progress on task (#354)
Shows the task completion percent as progress bar in task lists and on kanban cards.

Reviewed-on: vikunja/frontend#354
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: azymondrian <azymondrian@protonmail.com>
Co-committed-by: azymondrian <azymondrian@protonmail.com>
2020-12-31 15:16:07 +00:00
profi248 bb2800ec40 Improve editor buttons UX (#361)
Co-authored-by: konrad <konrad@kola-entertainments.de>
Reviewed-on: vikunja/frontend#361
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: profi248 <kostal.david8@gmail.com>
Co-committed-by: profi248 <kostal.david8@gmail.com>
2020-12-30 21:52:43 +00:00
profi248 8fe362c267 Show an icon if a task has non-empty description (Kanban view and List view) (#360)
Add an icon to indicate that task has a description (similar to Trello). Would also be nice to add a counter for comments, but it's not possible to reasonably currently implement unless the API also gets changed.

Also add attachment icon to List view, and change the icon in Kanban view to be consistent with the rest of the icon set.

Reviewed-on: vikunja/frontend#360
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: profi248 <kostal.david8@gmail.com>
Co-committed-by: profi248 <kostal.david8@gmail.com>
2020-12-30 21:20:33 +00:00
kolaente 624e4e6d27
Fix password reset 2020-12-30 21:43:43 +01:00
profi248 60c21cc36a Add "new label" button to label management (#359)
Allow to create labels directly from Manage labels page. It uses the same fullscreen dialog style as adding other things.

Almost all of the code is reused the `NewTeam` component

Co-authored-by: David Košťál <kostal.david8@gmail.com>
Reviewed-on: vikunja/frontend#359
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: profi248 <kostal.david8@gmail.com>
Co-committed-by: profi248 <kostal.david8@gmail.com>
2020-12-30 17:55:54 +00:00
15 changed files with 229 additions and 44 deletions

View File

@ -165,7 +165,7 @@ describe('Task', () => {
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.type('{selectall}New Description')
cy.get('.task-view .details.content.description .editor a')
.contains('Preview')
.contains('Done')
.click()
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')

View File

@ -73,14 +73,14 @@ export default {
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine))
},
setupPasswortResetRedirect() {
if (this.$route.query.userPasswordReset !== undefined) {
if (typeof this.$route.query.userPasswordReset !== 'undefined') {
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
this.$router.push({name: 'user.password-reset.reset'})
}
},
setupEmailVerificationRedirect() {
if (this.$route.query.userEmailConfirm !== undefined) {
if (typeof this.$route.query.userEmailConfirm !== 'undefined') {
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
this.$router.push({name: 'user.login'})

View File

@ -33,7 +33,9 @@ export default {
this.$route.name !== 'user.password-reset.reset' &&
this.$route.name !== 'user.register' &&
this.$route.name !== 'link-share.auth' &&
this.$route.name !== 'openid.auth'
this.$route.name !== 'openid.auth' &&
localStorage.getItem('passwordResetToken') === null &&
localStorage.getItem('emailConfirmToken') === null
) {
this.$router.push({name: 'user.login'})
}

View File

@ -2,11 +2,9 @@
<div :class="{'is-pulled-up': isEditEnabled}" class="editor">
<div class="tabs is-right" v-if="hasPreview && isEditEnabled && !hasEditBottom">
<ul>
<li :class="{'is-active': isPreviewActive}" v-if="isEditActive">
<a @click="showPreview">Preview</a>
</li>
<li :class="{'is-active': isEditActive}">
<a @click="() => {isPreviewActive = false; isEditActive = true}">Edit</a>
<li>
<a v-if="!isEditActive" @click="toggleEdit">Edit</a>
<a v-else @click="toggleEdit">Done</a>
</li>
</ul>
</div>
@ -23,17 +21,15 @@
</div>
<ul class="actions">
<template v-if="hasEditBottom">
<li>
<a v-if="!isEditActive" @click="toggleEdit">Edit</a>
<a v-else @click="toggleEdit">Done</a>
</li>
</template>
<li v-for="(action, k) in bottomActions" :key="k">
<a @click="action.action">{{ action.title }}</a>
</li>
<template v-if="hasEditBottom">
<li :class="{'is-active': isPreviewActive}" v-if="isEditActive">
<a @click="showPreview">Preview</a>
</li>
<li :class="{'is-active': isEditActive}">
<a @click="() => {isPreviewActive = false; isEditActive = true}">Edit</a>
</li>
</template>
</ul>
</div>
</template>
@ -394,10 +390,16 @@ export default {
this.bubble()
this.renderPreview()
},
showPreview() {
this.isPreviewActive = true
this.isEditActive = false
this.renderPreview()
toggleEdit() {
if (this.isEditActive) {
this.isPreviewActive = true;
this.isEditActive = false;
this.renderPreview();
this.bubble(0); // save instantly
} else {
this.isPreviewActive = false;
this.isEditActive = true;
}
},
},
}

View File

@ -47,7 +47,21 @@
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task"/>
</transition>
<priority-label :priority="task.priority"/>
<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>
</span>
<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 } }"
class="task-list"

View File

@ -19,6 +19,7 @@ import ListNamespaces from '../views/namespaces/ListNamespaces'
import ListTeamsComponent from '../views/teams/ListTeams'
// Label Handling
import ListLabelsComponent from '../views/labels/ListLabels'
import NewLabelComponent from '../views/labels/NewLabel'
// Migration
import MigrationComponent from '../views/migrator/Migrate'
import MigrateServiceComponent from '../views/migrator/MigrateService'
@ -253,6 +254,11 @@ export default new Router({
name: 'labels.index',
component: ListLabelsComponent,
},
{
path: '/labels/new',
name: 'labels.create',
component: NewLabelComponent,
},
{
path: '/migrate',
name: 'migrate.start',

View File

@ -72,6 +72,12 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1em - 1.5em - 8px';
font-size: .85rem;
word-break: break-word;
}
.progress {
margin: 8px 0 0 0;
width: 100%;
height: 0.5rem;
}
.due-date {
float: right;
@ -97,6 +103,7 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1em - 1.5em - 8px';
padding: 0;
display: flex;
justify-content: space-between;
margin-top: 8px;
.items {
display: flex;
@ -120,6 +127,20 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1em - 1.5em - 8px';
}
}
.icons-container {
display: flex;
align-items: flex-end;
.icon {
padding: 0px .4rem;
&:not(:first-child) {
margin-left: 4px;
}
svg {
margin: 0;
}
}
}
.icon {
svg {
margin: 4px 0 4px 4px;

View File

@ -254,3 +254,16 @@
.link-share-container .task-view {
background-color: transparent;
}
.flash-background {
animation: flash-background 0.75s ease 1;
}
@keyframes flash-background {
0% {
background: lighten($primary, 30);
}
100% {
background: transparent;
}
}

View File

@ -19,6 +19,7 @@
.task {
display: flex;
flex-wrap: wrap;
padding: 0.5rem 1rem;
border-bottom: 1px solid darken(#fff, 10%);
transition: background-color $transition;
@ -35,7 +36,7 @@
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
width: 100%;
flex: 1 0 50%;
.overdue {
color: $red;
@ -55,7 +56,7 @@
.color-bubble {
height: 10px;
flex: 1 0 10px;
flex: 0 0 10px;
}
.tag {
@ -70,6 +71,15 @@
width: 27px;
}
.list-task-icon {
margin-left: 6px;
&:not(:first-of-type) {
margin-left: 8px;
}
}
a {
color: $text;
transition: color ease $transition-duration;
@ -142,6 +152,18 @@
}
}
.progress {
width: 50px;
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%;
}
}
.task:last-child {
border-bottom: none;
}

View File

@ -164,7 +164,7 @@
.color-bubble {
height: 12px;
flex: 1 0 12px;
flex: 0 0 12px;
}
.favorite {

View File

@ -1,5 +1,11 @@
<template>
<div :class="{ 'is-loading': labelService.loading}" class="loader-container content">
<router-link :to="{name:'labels.create'}" class="button is-success button-right">
<span class="icon is-small">
<icon icon="plus"/>
</span>
New label
</router-link>
<h1>Manage labels</h1>
<p>
Click on a label to edit it.

View File

@ -0,0 +1,80 @@
<template>
<div class="fullpage">
<a @click="back()" class="close">
<icon :icon="['far', 'times-circle']"/>
</a>
<h3>Create a new label</h3>
<form @keyup.esc="back()" @submit.prevent="newlabel">
<div class="field is-grouped">
<p class="control is-expanded" v-bind:class="{ 'is-loading': labelService.loading }">
<input
:class="{ 'disabled': labelService.loading }"
class="input"
placeholder="The label title goes here..." type="text"
v-focus
v-model="label.title"/>
</p>
<p class="control">
<button class="button is-success noshadow" type="submit">
<span class="icon is-small">
<icon icon="plus"/>
</span>
Add
</button>
</p>
</div>
<p class="help is-danger" v-if="showError && label.title === ''">
Please specify a title.
</p>
</form>
</div>
</template>
<script>
import labelModel from '../../models/label'
import labelService from '../../services/label'
import {IS_FULLPAGE} from '@/store/mutation-types'
import LabelModel from '../../models/label'
import LabelService from '../../services/label'
export default {
name: 'NewLabel',
data() {
return {
labelService: labelService,
label: labelModel,
showError: false,
}
},
created() {
this.labelService = new LabelService()
this.label = new LabelModel()
this.$store.commit(IS_FULLPAGE, true)
},
mounted() {
this.setTitle('Create a new label')
},
methods: {
newlabel() {
if (this.label.title === '') {
this.showError = true
return
}
this.showError = false
this.labelService.create(this.label)
.then(response => {
this.$router.push({name: 'labels.index', params: {id: response.id}})
this.success({message: 'The label was successfully created.'}, this)
})
.catch(e => {
this.error(e, this)
})
},
back() {
this.$router.go(-1)
},
},
}
</script>

View File

@ -139,6 +139,12 @@
</span>
</span>
<h3>{{ task.title }}</h3>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
{{ task.percentDone * 100 }}%
</progress>
<labels :labels="task.labels"/>
<div class="footer">
<div class="items">
@ -153,16 +159,13 @@
/>
</div>
</div>
<div>
<span class="icon" v-if="task.attachments.length > 0">
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect fill="none" rx="0" ry="0"></rect>
<path
clip-rule="evenodd"
d="M19.86 8.29994C19.8823 8.27664 19.9026 8.25201 19.9207 8.22634C20.5666 7.53541 20.93 6.63567 20.93 5.68001C20.93 4.69001 20.55 3.76001 19.85 3.06001C18.45 1.66001 16.02 1.66001 14.62 3.06001L9.88002 7.80001C9.86705 7.81355 9.85481 7.82753 9.8433 7.8419L4.58 13.1C3.6 14.09 3.06 15.39 3.06 16.78C3.06 18.17 3.6 19.48 4.58 20.46C5.6 21.47 6.93 21.98 8.26 21.98C9.59 21.98 10.92 21.47 11.94 20.46L17.74 14.66C17.97 14.42 17.98 14.04 17.74 13.81C17.5 13.58 17.12 13.58 16.89 13.81L11.09 19.61C10.33 20.36 9.33 20.78 8.26 20.78C7.19 20.78 6.19 20.37 5.43 19.61C4.68 18.85 4.26 17.85 4.26 16.78C4.26 15.72 4.68 14.71 5.43 13.96L15.47 3.91996C15.4962 3.89262 15.5195 3.86346 15.54 3.83292C16.4992 2.95103 18.0927 2.98269 19.01 3.90001C19.48 4.37001 19.74 5.00001 19.74 5.67001C19.74 6.34001 19.48 6.97001 19.01 7.44001L14.27 12.18C14.2571 12.1935 14.2448 12.2075 14.2334 12.2218L8.96 17.4899C8.59 17.8699 7.93 17.8699 7.55 17.4899C7.36 17.2999 7.26 17.0399 7.26 16.7799C7.26 16.5199 7.36 16.2699 7.55 16.0699L15.47 8.14994C15.7 7.90994 15.71 7.52994 15.47 7.29994C15.23 7.06994 14.85 7.06994 14.62 7.29994L6.7 15.2199C6.29 15.6399 6.06 16.1899 6.06 16.7799C6.06 17.3699 6.29 17.9199 6.7 18.3399C7.12 18.7499 7.67 18.9799 8.26 18.9799C8.85 18.9799 9.4 18.7599 9.82 18.3399L19.86 8.29994Z"
fill-rule="evenodd"></path>
</svg>
</span>
<div class="icons-container">
<span class="icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span v-if="task.description" class="icon">
<icon icon="align-left"/>
</span>
</div>
</div>
</div>

View File

@ -28,7 +28,7 @@
v-model="task.assignees"
/>
</div>
<div class="column" v-if="activeFields.priority">
<div class="column" ref="priorityParent" v-if="activeFields.priority">
<!-- Priority -->
<div class="detail-title">
<icon :icon="['far', 'star']"/>
@ -40,7 +40,7 @@
ref="priority"
v-model="task.priority"/>
</div>
<div class="column" v-if="activeFields.dueDate">
<div class="column" ref="dueDateParent" v-if="activeFields.dueDate">
<!-- Due Date -->
<div class="detail-title">
<icon icon="calendar"/>
@ -61,7 +61,7 @@
</a>
</div>
</div>
<div class="column" v-if="activeFields.percentDone">
<div class="column" ref="percentDoneParent" v-if="activeFields.percentDone">
<!-- Percent Done -->
<div class="detail-title">
<icon icon="percent"/>
@ -73,7 +73,7 @@
ref="percentDone"
v-model="task.percentDone"/>
</div>
<div class="column" v-if="activeFields.startDate">
<div class="column" ref="startDateParent" v-if="activeFields.startDate">
<!-- Start Date -->
<div class="detail-title">
<icon icon="calendar-week"/>
@ -94,7 +94,7 @@
</a>
</div>
</div>
<div class="column" v-if="activeFields.endDate">
<div class="column" ref="endDateParent" v-if="activeFields.endDate">
<!-- End Date -->
<div class="detail-title">
<icon icon="calendar-week"/>
@ -115,7 +115,7 @@
</a>
</div>
</div>
<div class="column" v-if="activeFields.reminders">
<div class="column" ref="remindersParent" v-if="activeFields.reminders">
<!-- Reminders -->
<div class="detail-title">
<icon icon="history"/>
@ -127,7 +127,7 @@
ref="reminders"
v-model="task.reminderDates"/>
</div>
<div class="column" v-if="activeFields.repeatAfter">
<div class="column" ref="repeatAfterParent" v-if="activeFields.repeatAfter">
<!-- Repeat after -->
<div class="detail-title">
<icon :icon="['far', 'clock']"/>
@ -139,7 +139,7 @@
ref="repeatAfter"
v-model="task"/>
</div>
<div class="column" v-if="activeFields.color">
<div class="column" ref="colorParent" v-if="activeFields.color">
<!-- Color -->
<div class="detail-title">
<icon icon="fill-drip"/>
@ -546,7 +546,23 @@ export default {
this.activeFields[fieldName] = true
this.$nextTick(() => {
if (this.$refs[fieldName]) {
this.$refs[fieldName].$el.focus()
this.$refs[fieldName].$el.focus();
// animate background flash with CSS animations (if the field's parent has a ref)
if (typeof this.$refs[fieldName + "Parent"] !== 'undefined')
this.$refs[fieldName + "Parent"].classList.add('flash-background');
// remove animation class after animation finishes
setTimeout(() => {
if (typeof this.$refs[fieldName + "Parent"] !== 'undefined')
this.$refs[fieldName + "Parent"].classList.remove('flash-background');
}, 750);
// scroll the field to the center of the screen if not in viewport already
const boundingRect = this.$refs[fieldName].$el.getBoundingClientRect();
if (boundingRect.top > (window.scrollY + window.innerHeight) || boundingRect.top < window.scrollY)
this.$refs[fieldName].$el.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"});
}
})
},

View File

@ -96,7 +96,7 @@ export default {
let passwordReset = new PasswordResetModel({newPassword: this.credentials.password})
this.passwordResetService.resetPassword(passwordReset)
.then(response => {
this.successMessage = response.data.message
this.successMessage = response.message
localStorage.removeItem('passwordResetToken')
})
.catch(e => {