Skip to content

Commit fe557ac

Browse files
committed
Suggestions
1 parent 3bb1991 commit fe557ac

File tree

9 files changed

+197
-16
lines changed

9 files changed

+197
-16
lines changed

src/helpers/TreeModelHelper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ const TreeModelHelper = {
180180
});
181181
},
182182

183+
iterate<T extends ITreeItem>(items: T[], callback: (item: T) => void) {
184+
for (const item of items) {
185+
callback(item);
186+
if (item.children?.length) {
187+
callback(item);
188+
}
189+
}
190+
},
191+
183192
getItemRecursive<T extends ITreeItem>(
184193
items: T[],
185194
condition: (task: T) => boolean

src/modules/tasks/TaskStore.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { autorun, makeAutoObservable } from 'mobx';
1+
import { autorun, computed, makeAutoObservable, toJS } from 'mobx';
22
import { v4 as uuid } from 'uuid';
33
import TaskService from './TaskService';
44
import TaskModel, { ITimeRangeModel } from './models/TaskModel';
5-
import {
6-
Task,
7-
TasksByProject,
8-
} from '../../modules/tasks/models/TasksByProject';
5+
import { Task, TasksByProject } from './models/TasksByProject';
96
import TreeModelHelper from '../../helpers/TreeModelHelper';
107
import BadgeService from '../BadgeService';
118
import rootStore, { RootStore } from '../RootStore';
@@ -18,9 +15,12 @@ import {
1815
import { DEFAULT_PROJECT_ID } from '../projects/models/ProjectModel';
1916
import throttle from '../../helpers/Throttle';
2017
import { THROTTLE_SAVE_JSON_MS } from '../../config';
18+
import { findSuggestionsByProject } from './utils';
19+
import { Suggestion, SuggestionsByProject } from './models/TasksTypes';
2120

2221
export default class TaskStore {
2322
tasks: TasksByProject = {};
23+
suggestions: SuggestionsByProject = {};
2424
activeTask: TaskModel | undefined;
2525
versionHash = uuid();
2626
private tasksService = new TaskService();
@@ -65,6 +65,14 @@ export default class TaskStore {
6565
return this.tasks[projectId] || [];
6666
}
6767

68+
suggestionsForProject = computed<Suggestion[]>(() => {
69+
const { activeProject } = this.rootStore.projectStore;
70+
if (activeProject in this.suggestions) {
71+
return this.suggestions[activeProject] ?? [];
72+
}
73+
return [];
74+
});
75+
6876
getTaskByKey(taskKey: string): TaskModel | undefined {
6977
function condition(task: TaskModel): boolean {
7078
return task.key === taskKey;
@@ -163,6 +171,8 @@ export default class TaskStore {
163171

164172
restore() {
165173
this.tasks = this.tasksService.getAll();
174+
this.suggestions = findSuggestionsByProject(this.tasks);
175+
console.log(toJS(this.suggestions));
166176
this.findAndSetActiveTask();
167177
this.setupReminder(this.activeTask);
168178
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type SuggestionsByProject = Record<string, Suggestion[]>;
2+
3+
export type Suggestion = {
4+
text: string;
5+
frequency: number;
6+
};

src/modules/tasks/utils.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Task, TasksByProject } from './models/TasksByProject';
2+
import { Suggestion, SuggestionsByProject } from './models/TasksTypes';
3+
import TreeModelHelper from '../../helpers/TreeModelHelper';
4+
5+
export function findSuggestionsByProject(
6+
tasksByProject: TasksByProject
7+
): SuggestionsByProject {
8+
return Object.entries(tasksByProject).reduce<SuggestionsByProject>(
9+
(acc, [project, tasks]) => {
10+
const suggestions = findSuggestions(tasks);
11+
acc[project] = prepareSuggestions(suggestions);
12+
return acc;
13+
},
14+
{}
15+
);
16+
}
17+
18+
function prepareSuggestions(suggestions: Suggestion[]) {
19+
return suggestions
20+
.filter((item) => item.frequency > 1)
21+
.sort((a, b) => a.frequency - b.frequency);
22+
}
23+
24+
function findSuggestions(tasks: Task[]) {
25+
const suggestions: Record<number, Suggestion> = {};
26+
27+
TreeModelHelper.iterate(tasks, (task) => {
28+
const text = task.title;
29+
// TODO ignore long strings?
30+
const hash = hashCode(text);
31+
if (hash in suggestions) {
32+
suggestions[hash].frequency++;
33+
} else {
34+
suggestions[hash] = {
35+
text,
36+
frequency: 1,
37+
};
38+
}
39+
});
40+
41+
return Object.values(suggestions);
42+
}
43+
44+
// from https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
45+
function hashCode(str: string) {
46+
let hash = 0;
47+
let i, chr;
48+
if (str.length === 0) return hash;
49+
for (i = 0; i < str.length; i++) {
50+
chr = str.charCodeAt(i);
51+
hash = (hash << 5) - hash + chr;
52+
hash |= 0; // Convert to 32bit integer
53+
}
54+
return hash;
55+
}

src/screens/projects/ProjectsScreen.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@ import DrawerTask from './components/DrawerTask/DrawerTask';
1818
import ProjectNode from './components/ProjectNode/ProjectNode';
1919
import EditProjectModal from './components/ProjectModals/EditProjectModal';
2020
import { first } from '../../helpers/ArrayHelper';
21+
import Suggestions from './components/Suggestions/Suggestions';
22+
import ObservableInput from './components/observable/ObservableInput';
2123

2224
const { Sider } = Layout;
2325

2426
const { tasksStore, projectStore } = rootStore;
2527

28+
const observableInput = new ObservableInput();
29+
30+
// const Suggestions = observer(() => <div>test</div>);
31+
2632
const TaskList = TreeList(
2733
() => tasksStore.getTasks(projectStore.activeProject),
2834
(list: TaskModel[]) => {
@@ -86,7 +92,7 @@ function clearEditableProject() {
8692
projectStore.setEditableProject(undefined);
8793
}
8894

89-
function Projects() {
95+
function ProjectsScreen() {
9096
const style = useStyles();
9197
const [showProjectModal, setShowProjectModal] = useState<boolean>(false);
9298
const [drawerVisible, setDrawerVisible] = useState<boolean>(false);
@@ -128,7 +134,8 @@ function Projects() {
128134
<div className={style.root}>
129135
<TaskList onSelect={handleSelectTask} />
130136
<div className={style.stickyTaskInput}>
131-
<TaskInput />
137+
<Suggestions input={observableInput} />
138+
<TaskInput input={observableInput} />
132139
</div>
133140
</div>
134141
</Layout>
@@ -146,7 +153,7 @@ function Projects() {
146153
);
147154
}
148155

149-
export default observer(Projects);
156+
export default observer(ProjectsScreen);
150157

151158
const useStyles = createUseStyles({
152159
sider: {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { FC, useCallback } from 'react';
2+
import { Button } from 'antd';
3+
import { observer } from 'mobx-react';
4+
5+
import ObservableInput from '../observable/ObservableInput';
6+
7+
type Props = {
8+
text: string;
9+
input: ObservableInput;
10+
};
11+
12+
const SuggestionComp: FC<Props> = ({ text, input }: Props) => {
13+
const handleApplySuggestion = useCallback(() => input.setSuggestion(text), [
14+
input,
15+
text,
16+
]);
17+
18+
return (
19+
<Button
20+
type="primary"
21+
shape="round"
22+
// icon={}
23+
size="small"
24+
onClick={handleApplySuggestion}
25+
>
26+
{text}
27+
</Button>
28+
);
29+
};
30+
31+
export default observer(SuggestionComp);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { FC, useMemo } from 'react';
2+
import { observer } from 'mobx-react';
3+
4+
import Suggestion from './Suggestion';
5+
import ObservableInput from '../observable/ObservableInput';
6+
import rootStore from '../../../../modules/RootStore';
7+
8+
type Props = {
9+
input: ObservableInput;
10+
};
11+
12+
const Suggestions: FC<Props> = ({ input }: Props) => {
13+
const suggestions = useMemo(
14+
() => rootStore.tasksStore.suggestionsForProject,
15+
[]
16+
).get();
17+
18+
return (
19+
<>
20+
{suggestions.map((suggestion) => (
21+
<Suggestion
22+
key={suggestion.text}
23+
text={suggestion.text}
24+
input={input}
25+
/>
26+
))}
27+
</>
28+
);
29+
};
30+
31+
export default observer(Suggestions);

src/screens/projects/components/TaskInput.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import React, { KeyboardEvent, useCallback, useState } from 'react';
1+
import React, { KeyboardEvent, useCallback, useEffect, useRef } from 'react';
22
import { Input } from 'antd';
33
import { observer } from 'mobx-react';
44
import { v4 as uuid } from 'uuid';
55

66
import rootStore from '../../../modules/RootStore';
77
import TaskModel from '../../../modules/tasks/models/TaskModel';
8+
import ObservableInput from './observable/ObservableInput';
89

910
interface Props {
1011
className?: string;
12+
input: ObservableInput;
1113
}
1214

13-
export default observer(function TaskInput({ className }: Props) {
14-
const [text, setText] = useState('');
15+
export default observer(function TaskInput({ className, input }: Props) {
16+
const inputRef = useRef<Input>(null);
17+
18+
useEffect(() => {
19+
inputRef.current?.focus();
20+
}, [input.focusTrigger]);
1521

1622
const handleKeyPress = useCallback(
1723
(event: KeyboardEvent) => {
@@ -21,7 +27,7 @@ export default observer(function TaskInput({ className }: Props) {
2127
tasksStore.add(
2228
new TaskModel({
2329
key: uuid(),
24-
title: text,
30+
title: input.value,
2531
projectId: projectStore.activeProject,
2632
active: false,
2733
time: [],
@@ -33,20 +39,23 @@ export default observer(function TaskInput({ className }: Props) {
3339
expanded: true,
3440
})
3541
);
36-
setText('');
42+
input.clear();
3743
}
3844
},
39-
[text]
45+
[input]
4046
);
4147

42-
const handleChange = useCallback((e: any) => setText(e.target.value), []);
48+
const handleChange = useCallback((e: any) => input.set(e.target.value), [
49+
input,
50+
]);
4351

4452
return (
4553
<Input
54+
ref={inputRef}
4655
className={className}
4756
placeholder="Create task..."
4857
onKeyPress={handleKeyPress}
49-
value={text}
58+
value={input.value}
5059
onChange={handleChange}
5160
/>
5261
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { makeAutoObservable } from 'mobx';
2+
3+
export default class ObservableInput {
4+
value: string = '';
5+
focusTrigger: boolean = false; // TODO better solution?
6+
7+
constructor() {
8+
makeAutoObservable(this);
9+
}
10+
11+
set(value: string) {
12+
return (this.value = value);
13+
}
14+
15+
setSuggestion(value: string) {
16+
this.set(value);
17+
this.focusTrigger = !this.focusTrigger;
18+
}
19+
20+
clear() {
21+
return (this.value = '');
22+
}
23+
}

0 commit comments

Comments
 (0)