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:
parent
da9031a6ba
commit
56aaf3c1a3
@ -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>
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
||||
|
33
front/client/helpers/CatchConflictDecorator.ts
Normal file
33
front/client/helpers/CatchConflictDecorator.ts
Normal 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
|
||||
}
|
35
front/client/mixins/conflictDialogMixin.ts
Normal file
35
front/client/mixins/conflictDialogMixin.ts
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user