feat(merge_component): add 3 ways merging

WIP 3 ways merging, can now merge from third source on web client
This commit is contained in:
pakintada@gmail.com 2024-03-10 19:10:08 +07:00
parent 1b96e298ee
commit 47357e7ab4
8 changed files with 284 additions and 101 deletions

View file

@ -7,6 +7,7 @@ import { AppComponent } from './app.component';
import { ErrorInterceptor } from './core/interceptors/error.interceptor';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [AppComponent],
imports: [

View file

@ -8,6 +8,9 @@ export enum UserPermissions {
VIEWER = 1 << 4,
EDITOR = 1 << 7,
// add new permission by shifting after 7. eg. 8,9,...
// also do add at server
SUPER_ADMIN_PERMISSION = THAI_PERMISSION | MALAY_PERMISSION | AUS_PERMISSION | ALPHA3_PERMISSION | (EDITOR | VIEWER)
}

View file

@ -61,6 +61,7 @@ import { getCountryMapSwitcher } from 'src/app/shared/helpers/recipe';
export class DepartmentComponent {
acccessibleCountries:string[] = [];
// top row country
countries: { id: string; img: string }[] = [
{
id: 'tha',
@ -74,8 +75,10 @@ export class DepartmentComponent {
id: 'aus',
img: 'assets/departments/aus_plate.png',
},
// add new country here!
];
// bottom row country
alphas: { id: string; img: string }[] = [
{
id: 'alpha-3',

View file

@ -156,6 +156,11 @@
</div>
</div>
<!-- left as base -->
<div class="flex flex-row tooltip justify-evenly w-52">
<input type="checkbox" id="left-as-base">
<p class="tooltip" data-tip="Saved recipe from web has more priority than the machine">Main Priority</p>
</div>
<details class="collapse collapse-arrow">
<summary class="collapse-title bg-red-200">
{{ selectedCommit }}
@ -200,6 +205,11 @@
</div>
<div id="another-source" *ngIf="hasProductCodeOfCommits() && anotherSelectedSource != ''">
<!-- right as base -->
<div class="flex flex-row justify-evenly w-52">
<input type="checkbox" id="right-as-base">
<p class="tooltip" data-tip="Recipe from machine has more priority than the saved recipe from web">Main Priority</p>
</div>
<div *ngFor="let pd of getProductCodesOfCommits()">
<div *ngIf="pd == getCommitAttr(selectedCommit, 'contents').productCode && anotherTargetRecipe[pd] != undefined">
<details class="collapse collapse-arrow">

View file

@ -35,7 +35,6 @@ import { RecipeListComponent } from '../recipes/recipe-details/recipe-list/recip
import { ResizeEvent, ResizableModule } from 'angular-resizable-element';
import { Debugger } from 'src/app/shared/helpers/debugger';
@Component({
selector: 'app-merge',
standalone: true,
@ -53,22 +52,27 @@ import { Debugger } from 'src/app/shared/helpers/debugger';
ResizableModule,
],
})
export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges {
export class MergeComponent
implements OnInit, AfterViewInit, OnDestroy, OnChanges
{
// from web client
onRecipeListFormChange($event: any) {
console.log("left side change: ", $event)
console.log('left side change: ', $event);
// check if event[1] is an array of 30 elements or more
if (!Array.isArray($event[0])) {
// event contains index of selection
this.ignoredMap[this.selectedCommit] = $event;
this.selectionMap[this.selectedCommit + '_commit'] = $event;
}
}
// from another source
onSourceListFormChange($event: any) {
console.log("right side change: ", $event)
console.log('right side change: ', $event);
if (!Array.isArray($event[0])) {
this.selectionMap[this.selectedCommit + '_diff3'] = $event;
}
}
// input from layout::getSavedTmp
@ -94,7 +98,7 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
selectedProductCode = '';
changePackage:
{
| {
highlightChangeWhenMatched: {};
targetProductCode: string;
path: string;
@ -105,7 +109,7 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
selectedCommit: string = '';
anotherSelectedSource: string = '';
ignoredMap: any = {};
selectionMap: any = {};
// --------------------- Portal
// deprecated~!
@ -125,55 +129,51 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// --- debug
mergeDebugger = new Debugger();
constructor(
private _recipeService: RecipeService
)
{}
constructor(private _recipeService: RecipeService) {}
async ngOnChanges(changes: SimpleChanges): Promise<void> {
this._recipeService.getPatchListOfCurrentFile(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile()
)
.subscribe({
next: (data: any) => {
this.patchMap = data;
// console.log("patches",this.patchMap);
// console.log("commits", this.commit!);
this.getPatchMapKeys().forEach(async (key) => {
this.fullPatches[key] = {
contents: this.patchMap[key],
...this.commit!.find((commit: any) => commit.Id === key),
};
this._recipeService
.getPatchListOfCurrentFile(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile()
)
.subscribe({
next: (data: any) => {
this.patchMap = data;
// console.log("patches",this.patchMap);
// console.log("commits", this.commit!);
this.getPatchMapKeys().forEach(async (key) => {
this.fullPatches[key] = {
contents: this.patchMap[key],
...this.commit!.find((commit: any) => commit.Id === key),
};
// find product code from commits
let productCode = this.patchMap[key].productCode;
if (productCode) {
// check if exist in map
if (!this.mapProductCodeWithCommits[productCode]) {
this.mapProductCodeWithCommits[productCode] = [key];
} else {
this.mapProductCodeWithCommits[productCode].push(key);
}
}
// find product code from commits
let productCode = this.patchMap[key].productCode;
if (productCode) {
// check if exist in map
if (!this.mapProductCodeWithCommits[productCode]) {
this.mapProductCodeWithCommits[productCode] = [key];
} else {
this.mapProductCodeWithCommits[productCode].push(key);
}
}
// store only
await this.getMasterRecipeOfProductCode(productCode!);
// store only
await this.getMasterRecipeOfProductCode(productCode!);
// console.log("get master-commits recipe", this.mapProductCodeWithCommits);
});
// console.log("get master-commits recipe", this.mapProductCodeWithCommits);
console.log('full patch', this.fullPatches);
// console.log('change map', this.changeMap);
},
complete: () => {
console.log('full patch on complete', this.fullPatches);
},
});
console.log('full patch', this.fullPatches);
// console.log('change map', this.changeMap);
},
complete: () => {
console.log("full patch on complete", this.fullPatches);
},
});
}
async ngOnInit(): Promise<void> {
// fetch related product codes
}
@ -187,10 +187,8 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// this.appRef,
// this.injec2tor
// );
// this.host.attach(this.portal);
// console.log("Portal attached!");
// this.templatePortal = new TemplatePortal(
// this.templateRecipeChangeContent,
// this._containerRef
@ -208,7 +206,8 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// check if load patch keys
isPatchMapKeysLoaded = () => this.getPatchMapKeys().length > 0;
testLoadCheck = () => console.log('test load check', this.isPatchMapKeysLoaded());
testLoadCheck = () =>
console.log('test load check', this.isPatchMapKeysLoaded());
// get product code targets
getProductCodesOfCommits = () => Object.keys(this.mapProductCodeWithCommits);
@ -225,7 +224,7 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// console.log("test full patches ", this.fullPatches[id][attr]);
if (this.fullPatches[id] == undefined) {
return "";
return '';
}
return this.fullPatches[id][attr];
@ -265,36 +264,45 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
selectCommit = (commit: any) => {
// console.log('select commit', commit.target.value);
this.selectedCommit = commit.target.value;
}
};
selectAnotherSource = (source: any) => {
this.anotherSelectedSource = source.target.value;
}
};
changeAnotherSource = async (source: any) => {
this.anotherSelectedSource = "coffeethai02_" + source.target.value+".json";
console.log("another source: target version -> ", this.anotherSelectedSource);
this.anotherSelectedSource =
'coffeethai02_' + source.target.value + '.json';
console.log(
'another source: target version -> ',
this.anotherSelectedSource
);
// activate fetch
for(let pd of this.getProductCodesOfCommits()){
for (let pd of this.getProductCodesOfCommits()) {
await this.getAnotherRecipeOfProductCode(pd);
}
}
};
// get recipe from machine
// -- check ignore
isIgnoreListEmpty = () => Object.keys(this.ignoredMap).length === 0;
isCommitHasIgnored = () => this.ignoredMap[this.selectedCommit] != undefined && this.ignoredMap[this.selectedCommit].length > 0;
isIgnoreListEmpty = () => Object.keys(this.selectionMap).length === 0;
isCommitHasIgnored = () =>
this.selectionMap[this.selectedCommit] != undefined &&
this.selectionMap[this.selectedCommit].length > 0;
isNotSelectable = () => this.selectedCommit.startsWith('---');
buildContext = (ctxType?:string, pd?: string) => {
if(this.selectedCommit == "" || this.selectedCommit == undefined || this.selectedCommit.startsWith('---')){
buildContext = (ctxType?: string, pd?: string) => {
if (
this.selectedCommit == '' ||
this.selectedCommit == undefined ||
this.selectedCommit.startsWith('---')
) {
return {
changeContext: undefined,
skipZeroes: true,
toppingData: undefined,
}
};
}
// console.log("read commit contents",this.getCommitAttr(this.selectedCommit, 'contents').ToppingSet);
let patchData = this.fullPatches[this.selectedCommit];
@ -305,15 +313,15 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// };
if(ctxType != undefined){
switch(ctxType){
case "master":
if (ctxType != undefined) {
switch (ctxType) {
case 'master':
return {
changeContext: undefined,
skipZeroes: true,
toppingData: this.targetRecipe[pd!].ToppingSet,
};
case "right":
case 'right':
return {
changeContext: undefined,
skipZeroes: true,
@ -322,11 +330,11 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
}
}
return {
changeContext: undefined,
skipZeroes: true,
toppingData: this.getCommitAttr(this.selectedCommit, 'contents').ToppingSet,
toppingData: this.getCommitAttr(this.selectedCommit, 'contents')
.ToppingSet,
};
};
@ -386,7 +394,7 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
['LastChange']
);
// save only what changes
this.changeMap[patchId+"_"+this.anotherSelectedSource] = {
this.changeMap[patchId + '_' + this.anotherSelectedSource] = {
changes: cmp,
};
});
@ -410,18 +418,31 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// use with 'apply' button
async sendApply() {
console.log("test send apply", this.fullPatches[this.selectedCommit])
console.log('test send apply', this.fullPatches[this.selectedCommit]);
let commitKey = this.getCommitAttr(this.selectedCommit, 'Change_file');
let to_send: any = {
changeKey: commitKey
}
changeKey: commitKey,
};
// add appliedMachineRecipe if this is 3 ways merge
if(Object.keys(this.recipeFromMachine).length > 0){
to_send["appliedMachineRecipe"] = this.recipeFromMachine;
if (!this.isSelectionEmpty('_commit') || !this.isSelectionEmpty('_diff3')) {
this.setValueFromSelection();
} else if (
this.isSelectionEmpty('_commit') &&
this.isSelectionEmpty('_diff3')
) {
let checkPriority = this.getMergePriority();
if (checkPriority != '') {
// compare and set
this.compareAndSet(checkPriority);
}
}
if (Object.keys(this.recipeFromMachine).length > 0) {
to_send['appliedMachineRecipe'] = this.recipeFromMachine;
}
// multi-target changes
@ -429,29 +450,177 @@ export class MergeComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
// }
console.log("sending upgrade", to_send);
// do merge on client before apply to server if there is multiple requests.
this._recipeService.upgradeRecipe(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile(),
to_send
).subscribe({
next: (value) => {
// do alert user with the new filename
console.log('sending upgrade', to_send);
// this is ok case
if((value.result as string).includes(".json")){
alert("Upgrade success! New recipe file is " + value.result);
window.location.reload();
} else {
// this is error case
alert(value.result);
}
}
});
this._recipeService
.upgradeRecipe(
await this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile(),
to_send
)
.subscribe({
next: (value) => {
// do alert user with the new filename
// this is ok case
if ((value.result as string).includes('.json')) {
alert('Upgrade success! New recipe file is ' + value.result);
window.location.reload();
} else {
// this is error case
alert(value.result);
}
},
});
}
// merge on client if multiple
getMergePriority = () => {
// find id "left-as-base" and "right-as-base"
let commitPriority = document.getElementById('left-as-base') as any;
let diffPriority = document.getElementById('right-as-base') as any;
if (commitPriority.checked && diffPriority.checked) {
throw Error('Cannot make 2 as bases at the same time');
}
if (commitPriority != undefined && commitPriority.checked) {
return 'commit';
} else if (diffPriority != undefined && diffPriority.checked) {
return 'diff';
}
return '';
};
// selection empty?
isSelectionEmpty(side: string): boolean {
return this.selectionMap[this.selectedCommit + side] == undefined || this.selectionMap[this.selectedCommit + side].length == 0;
}
// put value from selection to base
setValueFromSelection = () => {
let base = this.getMergePriority();
let updateIdxList = [];
// get current pd
let pd = this.getCommitAttr(this.selectedCommit, 'contents').productCode;
if (base != '') {
switch (base) {
case 'commit':
updateIdxList = this.selectionMap[
this.selectedCommit + '_diff3'
] as Array<any>;
// get value from the right side
let diff_recipe = this.anotherTargetRecipe[pd];
updateIdxList.forEach((idx) => {
this.getCommitAttr(this.selectedCommit, 'contents').recipes[idx] =
diff_recipe.recipes[idx];
// TODO: add topping sync, if is topping slot, set topping too!
});
this.recipeFromMachine = this.getCommitAttr(
this.selectedCommit,
'contents'
);
break;
case 'diff':
updateIdxList = this.selectionMap[
this.selectedCommit + '_commit'
] as Array<any>;
// get value from the left side
let commit_recipe = this.getCommitAttr(
this.selectedCommit,
'contents'
).recipes;
updateIdxList.forEach((idx) => {
this.anotherTargetRecipe[pd].recipes[idx] = commit_recipe[idx];
// TODO: add topping sync, if is topping slot, set topping too!
});
this.recipeFromMachine = this.anotherTargetRecipe[pd];
break;
}
}
};
// traverse map from single layer key
singleTraverse(source: any, key: string) {
let keyArr = key.split('.');
let res: any;
keyArr.forEach((kval) => {
if (!isNaN(parseInt(kval))) {
res = source[parseInt(kval)];
} else {
res = source[kval];
}
});
return res;
}
translateSingleThenSet(source: any, update: any, key: string) {
let updatedValue = this.singleTraverse(update, key);
let currentSource = source;
const keyArr = key.split('.');
for (let idx = 0; idx < keyArr.length; idx++) {
const currKey = keyArr[idx];
if (!isNaN(parseInt(currKey))) {
currentSource[currKey] = {};
} else if (!(currKey in currentSource)) {
currentSource[currKey] = {};
}
currentSource = currentSource[currKey];
}
currentSource[keyArr[keyArr.length - 1]] = updatedValue;
}
// if no selection but has checked main priority on either side
compareAndSet(side: string) {
let diffList = [];
let pd = this.getCommitAttr(this.selectedCommit, 'contents').productCode;
switch (side) {
case '_commit':
// get compare
diffList = compare(
this.getCommitAttr(this.selectedCommit, 'contents'),
this.anotherTargetRecipe[pd],
['LastChange']
);
diffList.forEach((diff) => {
this.translateSingleThenSet(
this.getCommitAttr(this.selectedCommit, 'contents'),
this.anotherTargetRecipe[pd],
diff
);
});
this.recipeFromMachine = this.getCommitAttr(this.selectedCommit, 'contents');
break;
case '_diff3':
// get compare
diffList = compare(
this.anotherTargetRecipe[pd],
this.getCommitAttr(this.selectedCommit, 'contents'),
['LastChange']
);
diffList.forEach((diff) => {
this.translateSingleThenSet(
this.anotherTargetRecipe[pd],
this.getCommitAttr(this.selectedCommit, 'contents'),
diff
);
});
this.recipeFromMachine = this.anotherTargetRecipe[pd];
break;
}
}
// ---------------
// attach debugger

View file

@ -291,11 +291,6 @@ export class RecipeDetailsComponent implements OnInit {
this.tpl[ti][0],
(this.rawRecipe as any).ToppingSet[ti]
);
// check length
// update raw recipe
(this.rawRecipe as any).ToppingSet[ti] = this.tpl[ti][0];
console.log(

View file

@ -542,6 +542,8 @@ export class RecipeListComponent implements OnInit, OnChanges {
);
this.stringParams[index] = stringParamList;
console.log("string param debugr")
}
console.log('string param debug change', this.stringParamData);

View file

@ -909,7 +909,7 @@ func (rr *RecipeRouter) upgradeVersion(w http.ResponseWriter, r *http.Request) {
// upgrade version
// merge from website
result, err := rr.data.Merge(countryID, filename, mode, changeKey, nil)
result, err := rr.data.Merge(countryID, filename, mode, changeKey, appliedMachineRecipe)
if err != nil {
rr.taoLogger.Log.Error("RecipeRouter.upgradeVersion", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)