Skip to content

Commit 714de85

Browse files
committed
feat: start the implementation of the Commands
1 parent df401eb commit 714de85

File tree

3 files changed

+249
-55
lines changed

3 files changed

+249
-55
lines changed

src/commands/commands.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/* eslint-disable max-classes-per-file */
2+
3+
type PostType = {
4+
action: 'post';
5+
name: string;
6+
};
7+
8+
type PatchType = {
9+
action: 'patch';
10+
id: string;
11+
};
12+
13+
type DeleteType = {
14+
action: 'delete';
15+
id: string;
16+
};
17+
18+
export type CommandDataType = {
19+
data: { [key: string]: unknown };
20+
} & (PostType | PatchType | DeleteType);
21+
22+
export type PostCommandDataType = {
23+
data: { [key: string]: unknown };
24+
} & PostType;
25+
26+
export type PatchCommandDataType = {
27+
data: { [key: string]: unknown };
28+
} & PatchType;
29+
30+
export type DeleteCommandDataType = {
31+
data: { [key: string]: unknown };
32+
} & DeleteType;
33+
34+
export interface APIContext<T> {
35+
patch(data: T): void;
36+
post(data: T): void;
37+
delete(data: T): void;
38+
}
39+
40+
export abstract class Command<T> {
41+
protected apiContext: APIContext<T>;
42+
43+
constructor(apiContext: APIContext<T>) {
44+
this.apiContext = apiContext;
45+
}
46+
47+
abstract execute(): void;
48+
49+
abstract undo(): void;
50+
51+
abstract getInfo(): string;
52+
}
53+
54+
type FormattedCommand<T> = {
55+
type: string;
56+
command: Command<T>;
57+
message: string;
58+
};
59+
60+
export class HistoryCommand<T extends CommandDataType> extends Command<T> {
61+
private prevState;
62+
63+
private currState;
64+
65+
constructor({
66+
prevState,
67+
currState,
68+
apiContext,
69+
}: {
70+
prevState?: T;
71+
currState: T;
72+
apiContext: APIContext<T>;
73+
}) {
74+
super(apiContext);
75+
this.prevState = prevState;
76+
this.currState = currState;
77+
}
78+
79+
// TODO: improve the way to chose post. patch and delete
80+
execute(): void {
81+
// check if it is the first data for the app
82+
if (this.currState.action === 'post') {
83+
this.apiContext.post(this.currState);
84+
} else {
85+
this.apiContext.patch(this.currState);
86+
}
87+
}
88+
89+
undo(): void {
90+
if (!this.prevState) {
91+
this.apiContext.delete(this.currState);
92+
} else {
93+
this.apiContext.patch(this.prevState);
94+
}
95+
}
96+
97+
getInfo(): string {
98+
return `Command ${JSON.stringify(this.currState)}`;
99+
}
100+
}
101+
102+
export class HistoryManager<T extends CommandDataType> {
103+
private prevStates: Command<T>[] = [];
104+
105+
private nextStates: Command<T>[] = [];
106+
107+
public execute(command: Command<T>): void {
108+
this.nextStates = [];
109+
command.execute();
110+
this.prevStates = [...this.prevStates, command];
111+
}
112+
113+
public redo(): void {
114+
if (!this.nextStates.length) return;
115+
116+
const lastCommand = this.nextStates[this.nextStates.length - 1];
117+
lastCommand.execute();
118+
119+
this.prevStates = [...this.prevStates, lastCommand];
120+
this.nextStates = this.nextStates.slice(0, -1);
121+
}
122+
123+
public undo(): void {
124+
if (!this.prevStates.length) return;
125+
126+
const lastCommand = this.prevStates[this.prevStates.length - 1];
127+
lastCommand.execute();
128+
129+
this.nextStates = [...this.nextStates, lastCommand];
130+
this.prevStates = this.prevStates.slice(0, -1);
131+
}
132+
133+
public formattedBackHistory(): FormattedCommand<T>[] {
134+
return this.prevStates.map((command) => ({
135+
type: 'undo',
136+
command,
137+
message: command.getInfo(),
138+
}));
139+
}
140+
141+
public formattedForwardHistory(): FormattedCommand<T>[] {
142+
return [...this.nextStates].reverse().map((command) => ({
143+
type: 'redo',
144+
command,
145+
message: command.getInfo(),
146+
}));
147+
}
148+
}

src/components/context/AppSettingContext.tsx

+40-24
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,28 @@ import React, { FC, PropsWithChildren, createContext, useMemo } from 'react';
22

33
import { AppSetting } from '@graasp/sdk';
44

5+
import {
6+
APIContext,
7+
CommandDataType,
8+
DeleteCommandDataType,
9+
PatchCommandDataType,
10+
PostCommandDataType,
11+
} from '@/commands/commands';
12+
513
import { hooks, mutations } from '../../config/queryClient';
614
import Loader from '../common/Loader';
715

8-
type PostAppSettingType = {
9-
data: { [key: string]: unknown };
10-
name: string;
11-
};
12-
13-
type PatchAppSettingType = {
14-
data: { [key: string]: unknown };
15-
id: string;
16-
};
17-
18-
type DeleteAppSettingType = {
19-
id: string;
20-
};
21-
2216
export type AppSettingContextType = {
23-
postAppSetting: (payload: PostAppSettingType) => void;
24-
patchAppSetting: (payload: PatchAppSettingType) => void;
25-
deleteAppSetting: (payload: DeleteAppSettingType) => void;
17+
settingContext: APIContext<CommandDataType>;
2618
appSettingArray: AppSetting[];
2719
};
2820

2921
const defaultContextValue = {
30-
postAppSetting: () => null,
31-
patchAppSetting: () => null,
32-
deleteAppSetting: () => null,
22+
settingContext: {
23+
patch: () => null,
24+
post: () => null,
25+
delete: () => null,
26+
},
3327
appSettingArray: [],
3428
};
3529

@@ -42,14 +36,36 @@ export const AppSettingProvider: FC<PropsWithChildren> = ({ children }) => {
4236
const { mutate: postAppSetting } = mutations.usePostAppSetting();
4337
const { mutate: patchAppSetting } = mutations.usePatchAppSetting();
4438
const { mutate: deleteAppSetting } = mutations.useDeleteAppSetting();
39+
40+
const settingContext: APIContext<CommandDataType> = useMemo(
41+
() => ({
42+
patch: (payload: PatchCommandDataType) => {
43+
patchAppSetting({
44+
data: { ...payload.data },
45+
id: payload.id,
46+
});
47+
},
48+
post: (payload: PostCommandDataType) => {
49+
postAppSetting({
50+
data: { ...payload.data },
51+
name: payload.name,
52+
});
53+
},
54+
delete: (payload: DeleteCommandDataType) => {
55+
deleteAppSetting({
56+
id: payload.id,
57+
});
58+
},
59+
}),
60+
[deleteAppSetting, patchAppSetting, postAppSetting],
61+
);
62+
4563
const contextValue: AppSettingContextType = useMemo(
4664
() => ({
47-
postAppSetting,
48-
patchAppSetting,
49-
deleteAppSetting,
65+
settingContext,
5066
appSettingArray: appSetting.data || [],
5167
}),
52-
[appSetting.data, deleteAppSetting, patchAppSetting, postAppSetting],
68+
[appSetting.data, settingContext],
5369
);
5470

5571
if (appSetting.isLoading) {

src/components/views/admin/BuilderView.tsx

+61-31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
33

44
import { Alert, Box, Button, Stack, Typography } from '@mui/material';
55

6+
import { HistoryCommand, HistoryManager } from '@/commands/commands';
67
import GraaspButton from '@/components/common/settings/GraaspButton';
78
import { TEXT_ANALYSIS } from '@/langs/constants';
89

@@ -100,8 +101,9 @@ const BuilderView: FC = () => {
100101
setIsClean(stateIsClean);
101102
};
102103

103-
const { patchAppSetting, postAppSetting, appSettingArray } =
104-
useAppSettingContext();
104+
// const { patchAppSetting, postAppSetting, appSettingArray } =
105+
const { appSettingArray, settingContext } = useAppSettingContext();
106+
const history = new HistoryManager();
105107

106108
useEffect(() => {
107109
if (isClean) {
@@ -117,48 +119,76 @@ const BuilderView: FC = () => {
117119
}
118120
}, [appSettingArray, isClean]);
119121

122+
const hasKeyChanged = (settingKey: SettingKey): boolean => {
123+
const { value, dataKey } = settings[settingKey];
124+
const appSettingDataValue = getAppSetting(appSettingArray, settingKey)
125+
?.data[dataKey];
126+
127+
if (dataKey === DATA_KEYS.KEYWORDS) {
128+
const k1 = value;
129+
const k2 = (appSettingDataValue ?? []) as Keyword[];
130+
131+
const isKeywordListEqual: boolean =
132+
k1.length === k2.length &&
133+
k1.every((e1) =>
134+
k2.some((e2) => e1.word === e2.word && e1.def === e2.def),
135+
);
136+
return !isKeywordListEqual;
137+
}
138+
139+
return value !== appSettingDataValue;
140+
};
141+
120142
const saveSettings = (): void => {
121143
settingKeys.forEach((settingKey) => {
122144
const appSetting = getAppSetting(appSettingArray, settingKey);
123145
const { value, dataKey } = settings[settingKey];
124146

125147
if (appSetting) {
126-
patchAppSetting({
127-
data: { [dataKey]: value },
128-
id: appSetting.id,
129-
});
148+
// patchAppSetting({
149+
// data: { [dataKey]: value },
150+
// id: appSetting.id,
151+
// });
152+
// TODO: move the history in on changed instead of saved to have previous state
153+
if (!hasKeyChanged(settingKey)) {
154+
return;
155+
}
156+
157+
history.execute(
158+
new HistoryCommand({
159+
apiContext: settingContext,
160+
currState: {
161+
data: { [dataKey]: value },
162+
action: 'patch',
163+
id: appSetting.id,
164+
},
165+
}),
166+
);
130167
} else {
131-
postAppSetting({
132-
data: { [dataKey]: value },
133-
name: settingKey,
134-
});
168+
// postAppSetting({
169+
// data: { [dataKey]: value },
170+
// name: settingKey,
171+
// });
172+
history.execute(
173+
new HistoryCommand({
174+
apiContext: settingContext,
175+
currState: {
176+
data: { [dataKey]: value },
177+
action: 'post',
178+
name: settingKey,
179+
},
180+
}),
181+
);
135182
}
183+
184+
console.log(history.formattedBackHistory());
185+
console.log(history.formattedForwardHistory());
136186
});
137187

138188
setIsClean(true);
139189
};
140190

141-
const isChanged = settingKeys
142-
.map((settingKey) => {
143-
const { value, dataKey } = settings[settingKey];
144-
const appSettingDataValue = getAppSetting(appSettingArray, settingKey)
145-
?.data[dataKey];
146-
147-
if (dataKey === DATA_KEYS.KEYWORDS) {
148-
const k1 = value;
149-
const k2 = (appSettingDataValue ?? []) as Keyword[];
150-
151-
const isKeywordListEqual: boolean =
152-
k1.length === k2.length &&
153-
k1.every((e1) =>
154-
k2.some((e2) => e1.word === e2.word && e1.def === e2.def),
155-
);
156-
return !isKeywordListEqual;
157-
}
158-
159-
return value !== appSettingDataValue;
160-
})
161-
.some((v) => v);
191+
const isChanged = settingKeys.map(hasKeyChanged).some((v) => v);
162192

163193
return (
164194
<Stack

0 commit comments

Comments
 (0)