1
1
mirror of https://github.com/aelve/guide.git synced 2024-11-23 12:15:06 +03:00

front: conflict dialogs for every case, new interface of conflict catching

This commit is contained in:
zeot 2019-02-14 20:00:04 +04:00
parent da9031a6ba
commit 56aaf3c1a3
6 changed files with 153 additions and 115 deletions

View File

@ -11,7 +11,7 @@
toolbar
:value="categoryDscMarkdown"
@cancel="toggleEditDescription"
@save="saveDescription"
@save="updateDescription({original: originalDescription, modified: $event})"
/>
<v-btn
@ -25,13 +25,6 @@
<v-icon size="14" class="mr-1" left>{{descriptionBtnIcon}}</v-icon>
{{descriptionBtnText}}
</v-btn>
<conflict-dialog
v-model="isDescriptionConflict"
:serverModified="serverModified"
:modified="modified"
:merged="merged"
@saveDescription="saveConflictDescription"
/>
</div>
</template>
@ -39,13 +32,14 @@
import { Vue, Component, Prop } from 'vue-property-decorator'
import _get from 'lodash/get'
import MarkdownEditor from 'client/components/MarkdownEditor.vue'
import ConflictDialog from 'client/components/ConflictDialog.vue'
import conflictDialogMixin from 'client/mixins/conflictDialogMixin'
import CatchConflictDecorator from 'client/helpers/CatchConflictDecorator'
@Component({
components: {
MarkdownEditor,
ConflictDialog
}
MarkdownEditor
},
mixins: [conflictDialogMixin]
})
export default class CategoryDescriptiom extends Vue {
editDescriptionShown: boolean = false
@ -78,40 +72,20 @@ export default class CategoryDescriptiom extends Vue {
const description = _get(this, '$store.state.category.category.description.html')
return description ? this.descriptionButtonText = 'edit description' : this.descriptionButtonText = 'add description'
}
toggleEditDescription () {
this.editDescriptionShown = !this.editDescriptionShown
}
async updateCategoryDescription (original, modified) {
try {
await this.$store.dispatch('categoryItem/updateCategoryDescription', {
id: this.categoryId,
original: original,
modified: modified
})
this.originalDescription = modified
} catch (err) {
if (err.response.status === 409) {
this.serverModified = err.response.data.server_modified
this.modified = err.response.data.modified
this.merged = err.response.data.merged
this.isDescriptionConflict = true
} else {
throw err
}
}
this.toggleEditDescription();
}
saveDescription(newValue: string) {
this.updateCategoryDescription(this.originalDescription, newValue)
}
saveConflictDescription (modified) {
this.updateCategoryDescription(this.serverModified, modified)
@CatchConflictDecorator
async updateDescription ({ original, modified }) {
await this.$store.dispatch('categoryItem/updateCategoryDescription', {
id: this.categoryId,
original,
modified
})
this.originalDescription = modified
this.toggleEditDescription()
}
}
</script>

View File

@ -14,7 +14,7 @@
<category-item-section
title="Summary"
:editText="summary.text"
@save="updateSummary"
@save="updateSummary({original: summary.text, modified: $event})"
>
<div
class="mb-2 category-item-summary"
@ -38,7 +38,7 @@
<category-item-section
title="Ecosystem"
:editText="ecosystem.text"
@save="updateEcosystem"
@save="updateEcosystem({original: ecosystem.text, modified: $event})"
>
<div v-html="ecosystem.html" />
</category-item-section>
@ -46,7 +46,7 @@
<category-item-section
title="Notes"
:editText="notes.text"
@save="updateNotes"
@save="updateNotes({original: notes.text, modified: $event})"
>
<v-btn
small
@ -98,13 +98,16 @@ import { ICategoryItem } from 'client/service/CategoryItem.ts'
import CategoryItemToolbar from 'client/components/CategoryItemToolbar.vue'
import CategoryItemSection from 'client/components/CategoryItemSection.vue'
import CategoryItemTraits from 'client/components/CategoryItemTraits.vue'
import conflictDialogMixin from 'client/mixins/conflictDialogMixin'
import CatchConflictDecorator from 'client/helpers/CatchConflictDecorator'
@Component({
components: {
CategoryItemToolbar,
CategoryItemSection,
CategoryItemTraits
}
},
mixins: [conflictDialogMixin]
})
export default class CategoryItem extends Vue {
// TODO get rid of so many props and pass the item fully
@ -131,29 +134,32 @@ export default class CategoryItem extends Vue {
this.isNoteExpanded = false
}
async updateSummary (newValue: string): Promise<void> {
@CatchConflictDecorator
async updateSummary ({ original, modified }): Promise<void> {
await this.$store.dispatch('categoryItem/updateItemSummary', {
id: this.itemUid,
original: this.summary.text,
modified: newValue
original,
modified
})
await this.$store.dispatch('category/reloadCategory')
}
async updateEcosystem (newValue: string): Promise<void> {
@CatchConflictDecorator
async updateEcosystem ({ original, modified }): Promise<void> {
await this.$store.dispatch('categoryItem/updateItemEcosystem', {
id: this.itemUid,
original: this.ecosystem.text,
modified: newValue
original,
modified
})
await this.$store.dispatch('category/reloadCategory')
}
async updateNotes (newValue: string): Promise<void> {
@CatchConflictDecorator
async updateNotes ({ original, modified }): Promise<void> {
await this.$store.dispatch('categoryItem/updateItemNotes', {
id: this.itemUid,
original: this.notes.text,
modified: newValue
original,
modified
})
await this.$store.dispatch('category/reloadCategory')
}

View File

@ -76,7 +76,7 @@
:value="trait.content.text"
:height="100"
@cancel="trait.isEdit = false"
@save="saveEdit(trait, $event)"
@save="saveEdit({trait, original: trait.content.text, modified: $event})"
/>
</li>
</ul>
@ -98,13 +98,17 @@ import Confirm from 'client/helpers/ConfirmDecorator'
import CategoryItemSection from 'client/components/CategoryItemSection.vue'
import CategoryItemBtn from 'client/components/CategoryItemBtn.vue'
import MarkdownEditor from 'client/components/MarkdownEditor.vue'
import conflictDialogMixin from 'client/mixins/conflictDialogMixin'
import CatchConflictDecorator from 'client/helpers/CatchConflictDecorator'
@Component({
components: {
CategoryItemSection,
CategoryItemBtn,
MarkdownEditor
}
},
mixins: [conflictDialogMixin]
})
export default class CategoryItemTraits extends Vue {
// TODO change [any] type
@ -114,7 +118,7 @@ export default class CategoryItemTraits extends Vue {
isEdit: boolean = false
isAddTrait: boolean = false
traitsModel: any[] = []
traitsModel = []
get title () {
return this.type === 'pro' ? 'Pros' : 'Cons'
@ -123,7 +127,7 @@ export default class CategoryItemTraits extends Vue {
@Watch('traits', {
immediate: true
})
setTraitsModel (traits: any[]) {
setTraitsModel (traits) {
this.traitsModel = _cloneDeep(traits)
this.traitsModel.forEach(x => this.$set(x, 'isEdit', false))
}
@ -132,12 +136,13 @@ export default class CategoryItemTraits extends Vue {
this.isEdit = !this.isEdit
}
async saveEdit (trait: any, modifiedText: string) {
@CatchConflictDecorator
async saveEdit ({ trait, original, modified }) {
await this.$store.dispatch('categoryItem/updateItemTrait', {
itemId: this.itemId,
traitId: trait.id,
original: trait.content.text,
modified: modifiedText
original,
modified
})
trait.isEdit = false
await this.$store.dispatch('category/reloadCategory')

View File

@ -1,7 +1,7 @@
<template>
<v-dialog
:value="value"
@input="close"
persistent
max-width="99vw"
>
<slot slot="activator" />
@ -18,7 +18,7 @@
<v-btn
depressed
small
@click="saveUserVersion"
@click="save(modified)"
>
Submit this version, disregard changes on server
</v-btn>
@ -34,7 +34,7 @@
<v-btn
depressed
small
@click="saveServerVersion"
@click="save(serverModified)"
>
Accept this version, disregard my changes
</v-btn>
@ -45,8 +45,7 @@
class="mb-2"
toolbar
:value="merged"
@save="saveMerged"
@cancel="close"
@save="save"
/>
</div>
</div>
@ -68,67 +67,53 @@ export default class ConflictDialog extends Vue {
@Prop(String) modified!: string
@Prop(String) merged!: string
saveUserVersion () {
this.$emit('saveDescription', this.modified )
this.close()
save (newValue: string) {
this.$emit('save', newValue)
}
saveServerVersion () {
this.$emit('saveDescription', this.serverModified)
this.close()
}
saveMerged (newVal: string) {
this.$emit('saveDescription', this.serverModified)
this.close()
}
close ():void {
this.$emit('input', false)
}
}
</script>
<style scoped>
.conflict-box {
display: flex;
background: #fff;
padding: 20px;
justify-content: space-between;
}
.conflict-content {
flex: 1;
margin-bottom: 16px;
white-space: pre-wrap;
}
.conflict-item {
width: 32%;
display: flex;
flex-flow: column;
}
@media screen and (max-width: 1200px) {
.conflict-box {
display: flex;
background: #fff;
padding: 20px;
justify-content: space-between;
}
.conflict-content {
flex: 1;
margin-bottom: 16px;
white-space: pre-wrap;
flex-wrap: wrap;
}
.conflict-item {
width: 32%;
display: flex;
width: 49%;
}
.conflict-item:nth-last-child(1) {
width: 98%;
}
}
@media screen and (max-width: 768px) {
.conflict-box {
flex-flow: column;
}
@media screen and (max-width: 1200px) {
.conflict-box {
flex-wrap: wrap;
}
.conflict-item {
width: 49%;
}
.conflict-item:nth-last-child(1) {
width: 98%;
}
}
@media screen and (max-width: 768px) {
.conflict-box {
flex-flow: column;
}
.conflict-item {
width: 100%;
}
.conflict-item {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,33 @@
/**
* Decorator is used for functions that update string values such as category description, category item summary/ecosystem/trait, etc.
* Functions must have one object argument with at least two properties: 'original' and 'modified'.
* This properties are used in every API request that can have conflicts.
* Also, decorator needs 'openConflictDialog' function to be defined in component. For this you can add conflictDialogMixin or define your own function to resolve conflict.
*/
export default function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalFunction = descriptor.value
async function catchConflict (argsObject: object) {
try {
await originalFunction.call(this, argsObject)
} catch (err) {
if (err.response && err.response.status === 409) {
const serverModified = err.response.data.server_modified
const modified = err.response.data.modified
const merged = err.response.data.merged
const resolvedConflict = await this.openConflictDialog({ serverModified, modified, merged })
// We use this function again in case of new conflict occurs after resolving this one
catchConflict.call(this, {
...argsObject,
original: serverModified,
modified: resolvedConflict
})
} else {
throw err
}
}
}
descriptor.value = catchConflict
}

View File

@ -0,0 +1,35 @@
import Vue from 'vue'
import { Mixin } from 'vue-mixin-decorator'
import ConflictDialog from 'client/components/ConflictDialog.vue'
import DeferredPromise from 'client/helpers/DeferredPromise'
const ComponentClass = Vue.extend(ConflictDialog)
interface IConflictDialogProps {
serverModified: string,
modified: string,
merged: string
}
@Mixin
export default class ConflictDialogMixin extends Vue {
async openConflictDialog ({ serverModified, modified, merged }: IConflictDialogProps): Promise<string> {
const instance = new ComponentClass({
propsData: {
value: true,
serverModified,
modified,
merged
}
})
instance.$mount()
const deferredPromise = new DeferredPromise()
this.$el.appendChild(instance.$el)
instance.$on('save', (newVal) => {
instance.$destroy()
deferredPromise.resolve(newVal)
})
return deferredPromise
}
}