@osdk/create-app 0.17.0-beta.0 → 0.17.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/build/browser/index.js +8 -2
- package/build/browser/index.js.map +1 -1
- package/build/cjs/index.cjs +8 -2
- package/build/cjs/index.cjs.map +1 -1
- package/build/esm/index.js +8 -2
- package/build/esm/index.js.map +1 -1
- package/build/esm/templates.d.ts.map +1 -1
- package/package.json +2 -2
- package/templates/template-tutorial-todo-aip-app/.eslintrc.cjs +18 -0
- package/templates/template-tutorial-todo-aip-app/README.md.hbs +34 -0
- package/templates/template-tutorial-todo-aip-app/index.html +15 -0
- package/templates/template-tutorial-todo-aip-app/package.json.hbs +31 -0
- package/templates/template-tutorial-todo-aip-app/public/aip-icon.svg +3 -0
- package/templates/template-tutorial-todo-aip-app/public/todo-aip-app.svg +19 -0
- package/templates/template-tutorial-todo-aip-app/src/AuthCallback.tsx +24 -0
- package/templates/template-tutorial-todo-aip-app/src/AuthenticatedRoute.tsx +33 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateProjectButton.module.css +3 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateProjectButton.tsx +36 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateProjectDialog.module.css +15 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateProjectDialog.tsx +74 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateTaskButton.module.css +3 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateTaskButton.tsx +38 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateTaskDialog.module.css +66 -0
- package/templates/template-tutorial-todo-aip-app/src/CreateTaskDialog.tsx +140 -0
- package/templates/template-tutorial-todo-aip-app/src/DeleteProjectButton.module.css +3 -0
- package/templates/template-tutorial-todo-aip-app/src/DeleteProjectButton.tsx +37 -0
- package/templates/template-tutorial-todo-aip-app/src/DeleteProjectDialog.module.css +3 -0
- package/templates/template-tutorial-todo-aip-app/src/DeleteProjectDialog.tsx +57 -0
- package/templates/template-tutorial-todo-aip-app/src/Dialog.module.css +11 -0
- package/templates/template-tutorial-todo-aip-app/src/Dialog.tsx +19 -0
- package/templates/template-tutorial-todo-aip-app/src/Home.module.css +80 -0
- package/templates/template-tutorial-todo-aip-app/src/Home.tsx +135 -0
- package/templates/template-tutorial-todo-aip-app/src/Layout.module.css +16 -0
- package/templates/template-tutorial-todo-aip-app/src/Layout.tsx +23 -0
- package/templates/template-tutorial-todo-aip-app/src/Login.module.css +5 -0
- package/templates/template-tutorial-todo-aip-app/src/Login.tsx +44 -0
- package/templates/template-tutorial-todo-aip-app/src/ProjectSelect.tsx +40 -0
- package/templates/template-tutorial-todo-aip-app/src/TaskList.module.css +7 -0
- package/templates/template-tutorial-todo-aip-app/src/TaskList.tsx +44 -0
- package/templates/template-tutorial-todo-aip-app/src/TaskListItem.module.css +35 -0
- package/templates/template-tutorial-todo-aip-app/src/TaskListItem.tsx +58 -0
- package/templates/template-tutorial-todo-aip-app/src/client.ts.hbs +31 -0
- package/templates/template-tutorial-todo-aip-app/src/index.css +75 -0
- package/templates/template-tutorial-todo-aip-app/src/main.tsx +36 -0
- package/templates/template-tutorial-todo-aip-app/src/mocks.ts +187 -0
- package/templates/template-tutorial-todo-aip-app/src/useProjectTasks.ts +75 -0
- package/templates/template-tutorial-todo-aip-app/src/useProjects.ts +58 -0
- package/templates/template-tutorial-todo-aip-app/src/vite-env.d.ts +1 -0
- package/templates/template-tutorial-todo-aip-app/tsconfig.json +25 -0
- package/templates/template-tutorial-todo-aip-app/tsconfig.node.json +10 -0
- package/templates/template-tutorial-todo-aip-app/vite.config.ts.hbs +19 -0
- package/templates/template-tutorial-todo-app/README.md.hbs +1 -1
- package/templates/template-tutorial-todo-app/src/CreateProjectDialog.tsx +3 -3
- package/templates/template-tutorial-todo-app/src/CreateTaskDialog.tsx +3 -3
- package/templates/template-tutorial-todo-app/src/DeleteProjectDialog.tsx +3 -3
- package/templates/template-tutorial-todo-app/src/Dialog.module.css +5 -0
- package/templates/template-tutorial-todo-app/src/Dialog.tsx +1 -1
- package/templates/template-tutorial-todo-app/src/Home.module.css +1 -2
- package/templates/template-tutorial-todo-app/src/Home.tsx +4 -3
- package/templates/template-tutorial-todo-app/src/TaskListItem.module.css +10 -0
- package/templates/template-tutorial-todo-app/src/TaskListItem.tsx +11 -8
- package/templates/template-tutorial-todo-app/src/index.css +0 -5
- package/templates/template-tutorial-todo-app/src/mocks.ts +3 -3
- package/templates/template-tutorial-todo-app/src/useProjectTasks.ts +5 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.task {
|
|
2
|
+
display: grid;
|
|
3
|
+
grid-template-columns: 1fr 1fr;
|
|
4
|
+
grid-template-rows: auto min-content;
|
|
5
|
+
gap: 10px;
|
|
6
|
+
width: 500px;
|
|
7
|
+
}
|
|
8
|
+
.label {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 5px;
|
|
11
|
+
resize: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.input {
|
|
15
|
+
width: 300px;
|
|
16
|
+
border: 1px solid gray;
|
|
17
|
+
border-radius: 5px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.textArea {
|
|
21
|
+
width: 300px;
|
|
22
|
+
resize: vertical;
|
|
23
|
+
border: 1px solid gray;
|
|
24
|
+
border-radius: 5px;
|
|
25
|
+
padding: 5px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.aip {
|
|
29
|
+
background-color: #7961DB;
|
|
30
|
+
border: 1px solid #ccc;
|
|
31
|
+
padding: 2px 5px;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
align-items: center;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.processing {
|
|
38
|
+
cursor: progress;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.image {
|
|
42
|
+
width: 16px;
|
|
43
|
+
height: 16px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.container {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items:flex-start;
|
|
49
|
+
gap: 5px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.dialogContainer {
|
|
53
|
+
position: fixed;
|
|
54
|
+
top: 0;
|
|
55
|
+
right: 0;
|
|
56
|
+
bottom: 0;
|
|
57
|
+
left: 0;
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
align-items: center;
|
|
61
|
+
background-color: rgba(0, 0, 0, 0.5); /* Optional: for dimmed background */
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.button {
|
|
65
|
+
border: 1px solid #ccc;
|
|
66
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import aipLogo from "/aip-icon.svg";
|
|
2
|
+
import type { ChangeEvent } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import css from "./CreateTaskDialog.module.css";
|
|
5
|
+
import Dialog from "./Dialog";
|
|
6
|
+
import type { MockProject } from "./mocks";
|
|
7
|
+
import { useProjectTasks } from "./useProjectTasks";
|
|
8
|
+
|
|
9
|
+
interface CreateTaskDialogProps {
|
|
10
|
+
project: MockProject;
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
onTaskCreated: (taskId: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function CreateTaskDialog(
|
|
17
|
+
{ project, isOpen, onClose, onTaskCreated }: CreateTaskDialogProps,
|
|
18
|
+
) {
|
|
19
|
+
const { createTask, getRecommendedTaskDescription } = useProjectTasks(
|
|
20
|
+
project,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const [name, setName] = useState<string>("New task");
|
|
24
|
+
const [description, setDescription] = useState<string>("");
|
|
25
|
+
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
|
26
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
27
|
+
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
28
|
+
|
|
29
|
+
const handleChangeTaskName = useCallback(
|
|
30
|
+
(e: ChangeEvent<HTMLInputElement>) => setName(e.target.value),
|
|
31
|
+
[],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const handleChangeTaskDescription = useCallback(
|
|
35
|
+
(e: ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value),
|
|
36
|
+
[],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const handleTaskDescriptionRecommendation = useCallback(async () => {
|
|
40
|
+
setIsProcessing(true);
|
|
41
|
+
const recommendedDescription = await getRecommendedTaskDescription(name);
|
|
42
|
+
setDescription(recommendedDescription);
|
|
43
|
+
setIsProcessing(false);
|
|
44
|
+
}, [getRecommendedTaskDescription, name]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setName("New task");
|
|
48
|
+
setDescription("");
|
|
49
|
+
}, [isOpen]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (textAreaRef.current) {
|
|
52
|
+
const textArea = textAreaRef.current;
|
|
53
|
+
textArea.style.height = "auto";
|
|
54
|
+
textArea.style.height = `${textArea.scrollHeight}px`;
|
|
55
|
+
}
|
|
56
|
+
}, [description]);
|
|
57
|
+
|
|
58
|
+
const handleSubmit = useCallback(async () => {
|
|
59
|
+
setIsCreating(true);
|
|
60
|
+
try {
|
|
61
|
+
const taskId = await createTask(name, description);
|
|
62
|
+
if (taskId != null) {
|
|
63
|
+
onTaskCreated(taskId);
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
setIsCreating(false);
|
|
67
|
+
onClose();
|
|
68
|
+
}
|
|
69
|
+
}, [onClose, createTask, onTaskCreated, name, description]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<>
|
|
73
|
+
{isOpen && (
|
|
74
|
+
<div className={css.dialogContainer}>
|
|
75
|
+
<Dialog
|
|
76
|
+
isOpen={isOpen}
|
|
77
|
+
buttons={[
|
|
78
|
+
<button
|
|
79
|
+
disabled={isCreating}
|
|
80
|
+
onClick={onClose}
|
|
81
|
+
key="cancel"
|
|
82
|
+
className={css.button}
|
|
83
|
+
>
|
|
84
|
+
Cancel
|
|
85
|
+
</button>,
|
|
86
|
+
<button
|
|
87
|
+
disabled={isCreating}
|
|
88
|
+
onClick={handleSubmit}
|
|
89
|
+
key="create"
|
|
90
|
+
className={css.button}
|
|
91
|
+
>
|
|
92
|
+
Create task
|
|
93
|
+
</button>,
|
|
94
|
+
]}
|
|
95
|
+
>
|
|
96
|
+
<div className={css.task}>
|
|
97
|
+
<label className={css.label}>
|
|
98
|
+
Task name:{" "}
|
|
99
|
+
</label>
|
|
100
|
+
<input
|
|
101
|
+
type="text"
|
|
102
|
+
value={name}
|
|
103
|
+
onChange={handleChangeTaskName}
|
|
104
|
+
className={css.input}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
<label className={css.label}>
|
|
108
|
+
Task description:{" "}
|
|
109
|
+
</label>
|
|
110
|
+
<div className={css.container}>
|
|
111
|
+
<textarea
|
|
112
|
+
ref={textAreaRef}
|
|
113
|
+
value={description}
|
|
114
|
+
onChange={handleChangeTaskDescription}
|
|
115
|
+
className={css.textArea}
|
|
116
|
+
rows={2}
|
|
117
|
+
/>
|
|
118
|
+
<button
|
|
119
|
+
disabled={isProcessing}
|
|
120
|
+
className={`${css.aip} ${isProcessing ? css.processing : ""}`}
|
|
121
|
+
title="Click here to get AIP task description recommendation"
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={handleTaskDescriptionRecommendation}
|
|
124
|
+
>
|
|
125
|
+
<img
|
|
126
|
+
src={aipLogo}
|
|
127
|
+
alt="AIP"
|
|
128
|
+
className={css.image}
|
|
129
|
+
/>
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</Dialog>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default CreateTaskDialog;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import css from "./DeleteProjectButton.module.css";
|
|
3
|
+
import DeleteProjectDialog from "./DeleteProjectDialog";
|
|
4
|
+
import type { MockProject } from "./mocks";
|
|
5
|
+
import useProjects from "./useProjects";
|
|
6
|
+
|
|
7
|
+
interface DeleteProjectButtonProps {
|
|
8
|
+
project: MockProject;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function DeleteProjectButton({ project }: DeleteProjectButtonProps) {
|
|
12
|
+
const { isLoading: isLoadingProjects, isError: isErrorProjects } =
|
|
13
|
+
useProjects();
|
|
14
|
+
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
const handleOpen = useCallback(() => setIsOpen(true), []);
|
|
17
|
+
const handleClose = useCallback(() => setIsOpen(false), []);
|
|
18
|
+
|
|
19
|
+
if (isLoadingProjects || isErrorProjects) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<button onClick={handleOpen} className={css.button}>
|
|
26
|
+
Delete Project
|
|
27
|
+
</button>
|
|
28
|
+
<DeleteProjectDialog
|
|
29
|
+
project={project}
|
|
30
|
+
isOpen={isOpen}
|
|
31
|
+
onClose={handleClose}
|
|
32
|
+
/>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default DeleteProjectButton;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import css from "./DeleteProjectDialog.module.css";
|
|
3
|
+
import Dialog from "./Dialog";
|
|
4
|
+
import type { MockProject } from "./mocks";
|
|
5
|
+
import useProjects from "./useProjects";
|
|
6
|
+
|
|
7
|
+
interface DeleteProjectDialogProps {
|
|
8
|
+
project: MockProject;
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function DeleteProjectDialog({
|
|
14
|
+
project,
|
|
15
|
+
isOpen,
|
|
16
|
+
onClose,
|
|
17
|
+
}: DeleteProjectDialogProps) {
|
|
18
|
+
const { deleteProject } = useProjects();
|
|
19
|
+
|
|
20
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
21
|
+
const handleSubmit = useCallback(async () => {
|
|
22
|
+
setIsDeleting(true);
|
|
23
|
+
try {
|
|
24
|
+
await deleteProject(project);
|
|
25
|
+
} finally {
|
|
26
|
+
setIsDeleting(false);
|
|
27
|
+
onClose();
|
|
28
|
+
}
|
|
29
|
+
}, [deleteProject, onClose, project]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Dialog
|
|
33
|
+
isOpen={isOpen}
|
|
34
|
+
buttons={[
|
|
35
|
+
<button
|
|
36
|
+
disabled={isDeleting}
|
|
37
|
+
onClick={onClose}
|
|
38
|
+
key="cancel"
|
|
39
|
+
className={css.button}
|
|
40
|
+
>
|
|
41
|
+
Cancel
|
|
42
|
+
</button>,
|
|
43
|
+
<button
|
|
44
|
+
disabled={isDeleting}
|
|
45
|
+
onClick={handleSubmit}
|
|
46
|
+
key="delete"
|
|
47
|
+
className={css.button}
|
|
48
|
+
>
|
|
49
|
+
Delete
|
|
50
|
+
</button>,
|
|
51
|
+
]}
|
|
52
|
+
>
|
|
53
|
+
Are you sure you want to delete this project?
|
|
54
|
+
</Dialog>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
export default DeleteProjectDialog;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PropsWithChildren } from "react";
|
|
2
|
+
import css from "./Dialog.module.css";
|
|
3
|
+
|
|
4
|
+
interface DialogProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
buttons?: React.ReactElement[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function Dialog({ children, isOpen, buttons }: PropsWithChildren<DialogProps>) {
|
|
10
|
+
return (
|
|
11
|
+
<dialog open={isOpen} className={css.dialog}>
|
|
12
|
+
{children}
|
|
13
|
+
{buttons != null && buttons.length > 0 && (
|
|
14
|
+
<div className={css.buttons}>{buttons}</div>
|
|
15
|
+
)}
|
|
16
|
+
</dialog>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export default Dialog;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.tutorialBannerWrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
margin: 2em;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.tutorialBanner {
|
|
7
|
+
flex: 1;
|
|
8
|
+
width: 0;
|
|
9
|
+
background: rgba(121, 97, 219, 0.7);
|
|
10
|
+
color: #ffffff;
|
|
11
|
+
border-radius: 1em;
|
|
12
|
+
padding: 1em;
|
|
13
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.tutorialBannerTitle {
|
|
17
|
+
margin-top: 0;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.projectSelect {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 1em;
|
|
25
|
+
margin: 2em;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.projectCard {
|
|
30
|
+
margin: 2em;
|
|
31
|
+
padding: 0.5em;
|
|
32
|
+
border: 1px solid #ccc;
|
|
33
|
+
/* margin: 0.5em; */
|
|
34
|
+
gap: 0.5em;
|
|
35
|
+
border-radius: 5px;
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.projectTitle {
|
|
40
|
+
font-size: 1.5em;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.description {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.image {
|
|
49
|
+
height: 16px;
|
|
50
|
+
width: 16px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.aip {
|
|
54
|
+
background-color: #7961DB;
|
|
55
|
+
color: #ffffff;
|
|
56
|
+
border: 1px solid #ccc;
|
|
57
|
+
padding: 2px;
|
|
58
|
+
gap: 10px;
|
|
59
|
+
height: 30px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.processing {
|
|
63
|
+
cursor: progress;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.aipText {
|
|
67
|
+
display: flex;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
gap: 10px;
|
|
70
|
+
align-items: center;
|
|
71
|
+
padding: 0 10px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.textArea {
|
|
75
|
+
border: none;
|
|
76
|
+
color: gray;
|
|
77
|
+
resize: none;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import aipLogo from "/aip-icon.svg";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import CreateProjectButton from "./CreateProjectButton";
|
|
4
|
+
import CreateTaskButton from "./CreateTaskButton";
|
|
5
|
+
import DeleteProjectButton from "./DeleteProjectButton";
|
|
6
|
+
import css from "./Home.module.css";
|
|
7
|
+
import Layout from "./Layout";
|
|
8
|
+
import type { MockProject } from "./mocks";
|
|
9
|
+
import ProjectSelect from "./ProjectSelect";
|
|
10
|
+
import TaskList from "./TaskList";
|
|
11
|
+
import useProjects from "./useProjects";
|
|
12
|
+
import { useProjectTasks } from "./useProjectTasks";
|
|
13
|
+
|
|
14
|
+
function Home() {
|
|
15
|
+
const [projectId, setProjectId] = useState<string | undefined>(undefined);
|
|
16
|
+
const { projects, updateProjectDescription } = useProjects();
|
|
17
|
+
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
|
18
|
+
const [projectHasTasks, setProjectHasTasks] = useState<boolean>(false);
|
|
19
|
+
|
|
20
|
+
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
|
21
|
+
const project = projects?.find((p) => p.id === projectId);
|
|
22
|
+
const tasks = useProjectTasks(project).tasks;
|
|
23
|
+
|
|
24
|
+
const handleSelectProject = useCallback(
|
|
25
|
+
(p: MockProject) => setProjectId(p.id),
|
|
26
|
+
[],
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const handleProjectDescriptionRecommendation = useCallback(async () => {
|
|
30
|
+
if (project == null) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setIsProcessing(true);
|
|
34
|
+
await updateProjectDescription(project);
|
|
35
|
+
setIsProcessing(false);
|
|
36
|
+
}, [project, updateProjectDescription]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (project == null && projects != null && projects.length > 0) {
|
|
40
|
+
setProjectId(projects[0].id);
|
|
41
|
+
setProjectHasTasks(tasks == null ? false : tasks.length > 0);
|
|
42
|
+
}
|
|
43
|
+
}, [project, projects, tasks]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (textAreaRef.current) {
|
|
47
|
+
const textArea = textAreaRef.current;
|
|
48
|
+
textArea.style.height = "auto";
|
|
49
|
+
textArea.style.height = `${textArea.scrollHeight}px`;
|
|
50
|
+
}
|
|
51
|
+
}, [project?.description]);
|
|
52
|
+
|
|
53
|
+
const handleOnTaskCreated = useCallback(() => {
|
|
54
|
+
setProjectHasTasks(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
const handleOnProjectCreated = useCallback(
|
|
58
|
+
(projectId: string | undefined) => {
|
|
59
|
+
setProjectId(projectId);
|
|
60
|
+
setProjectHasTasks(false);
|
|
61
|
+
},
|
|
62
|
+
[],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleOnTaskDeleted = useCallback(() => {
|
|
66
|
+
if (tasks?.length === 0) {
|
|
67
|
+
setProjectHasTasks(false);
|
|
68
|
+
}
|
|
69
|
+
}, [tasks]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Layout>
|
|
73
|
+
<div className={css.tutorialBannerWrapper}>
|
|
74
|
+
<div className={css.tutorialBanner}>
|
|
75
|
+
<p className={css.tutorialBannerTitle}>
|
|
76
|
+
💡 Welcome to the To Do AIP App tutorial!
|
|
77
|
+
</p>
|
|
78
|
+
<p>
|
|
79
|
+
The application is implemented with mock in memory data.
|
|
80
|
+
<br />Can you solve how to change it to use the Ontology SDK
|
|
81
|
+
instead?
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div className={css.projectSelect}>
|
|
86
|
+
<span>Project:</span>
|
|
87
|
+
<ProjectSelect
|
|
88
|
+
project={project}
|
|
89
|
+
projects={projects ?? []}
|
|
90
|
+
onSelectProject={handleSelectProject}
|
|
91
|
+
/>
|
|
92
|
+
<CreateProjectButton onProjectCreated={handleOnProjectCreated} />
|
|
93
|
+
{project != null && <DeleteProjectButton project={project} />}
|
|
94
|
+
</div>
|
|
95
|
+
{project != null && (
|
|
96
|
+
<div className={css.projectCard} key={project.id}>
|
|
97
|
+
<h1 className={css.projectTitle}>{project.name}</h1>
|
|
98
|
+
{projectHasTasks && (
|
|
99
|
+
<div className={css.description}>
|
|
100
|
+
<textarea
|
|
101
|
+
ref={textAreaRef}
|
|
102
|
+
readOnly
|
|
103
|
+
value={project.description}
|
|
104
|
+
className={css.textArea}
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
disabled={isProcessing}
|
|
108
|
+
className={`${css.aip} ${isProcessing ? css.processing : ""}`}
|
|
109
|
+
title="Click here to update project description based on AIP Logic"
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={handleProjectDescriptionRecommendation}
|
|
112
|
+
>
|
|
113
|
+
<div className={css.aipText}>
|
|
114
|
+
<img
|
|
115
|
+
src={aipLogo}
|
|
116
|
+
alt="AIP"
|
|
117
|
+
className={css.image}
|
|
118
|
+
/>
|
|
119
|
+
Get description recommendation
|
|
120
|
+
</div>
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
<TaskList project={project} onTaskDeleted={handleOnTaskDeleted} />
|
|
125
|
+
<CreateTaskButton
|
|
126
|
+
project={project}
|
|
127
|
+
onTaskCreated={handleOnTaskCreated}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</Layout>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default Home;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import todoAppLogo from "/todo-aip-app.svg";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import css from "./Layout.module.css";
|
|
4
|
+
|
|
5
|
+
interface LayoutProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function Layout({ children }: LayoutProps) {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<div className={css.header}>
|
|
13
|
+
<img src={todoAppLogo} className={css.logo} alt="Todo App logo" />
|
|
14
|
+
<div className={css.title}>
|
|
15
|
+
Ontology SDK Tutorial - To Do App Powered by AIP
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
{children}
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default Layout;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Navigate } from "react-router-dom";
|
|
3
|
+
import client from "./client";
|
|
4
|
+
import Layout from "./Layout";
|
|
5
|
+
import css from "./Login.module.css";
|
|
6
|
+
|
|
7
|
+
function Login() {
|
|
8
|
+
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
|
9
|
+
const [error, setError] = useState<string | undefined>(undefined);
|
|
10
|
+
const token = client.auth.token;
|
|
11
|
+
|
|
12
|
+
const handleLogin = useCallback(async () => {
|
|
13
|
+
setIsLoggingIn(true);
|
|
14
|
+
try {
|
|
15
|
+
// Initiate the OAuth flow, which will redirect the user to log into Foundry
|
|
16
|
+
// Once the login has completed, the user will be redirected back to the route defined via the
|
|
17
|
+
// FOUNDRY_REDIRECT_URL variable in .env.development
|
|
18
|
+
await client.auth.signIn();
|
|
19
|
+
} catch (e: unknown) {
|
|
20
|
+
console.error(e);
|
|
21
|
+
setError((e as Error).message ?? e);
|
|
22
|
+
} finally {
|
|
23
|
+
setIsLoggingIn(false);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
// If the token exists but a user tries to load /login, redirect to the home page instead
|
|
28
|
+
if (token != null) {
|
|
29
|
+
return <Navigate to="/" replace={true} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Layout>
|
|
34
|
+
<div className={css.loginButton}>
|
|
35
|
+
<button onClick={handleLogin}>
|
|
36
|
+
{isLoggingIn ? "Logging in…" : "Log in "}
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
{error && <div>Unable to log in: {error}</div>}
|
|
40
|
+
</Layout>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default Login;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ChangeEvent, useCallback } from "react";
|
|
2
|
+
import { MockProject } from "./mocks";
|
|
3
|
+
|
|
4
|
+
interface ProjectSelectProps {
|
|
5
|
+
project: MockProject | undefined;
|
|
6
|
+
projects: MockProject[];
|
|
7
|
+
onSelectProject: (project: MockProject) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ProjectSelect({
|
|
11
|
+
project,
|
|
12
|
+
projects,
|
|
13
|
+
onSelectProject,
|
|
14
|
+
}: ProjectSelectProps) {
|
|
15
|
+
const handleSelect = useCallback(
|
|
16
|
+
(e: ChangeEvent<HTMLSelectElement>) => {
|
|
17
|
+
const nextProject = projects.find((p) => `${p.id}` === e.target.value);
|
|
18
|
+
if (nextProject != null) {
|
|
19
|
+
onSelectProject(nextProject);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
[projects, onSelectProject],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<select value={project?.id} onChange={handleSelect}>
|
|
27
|
+
<option hidden disabled value="">
|
|
28
|
+
-- select a project --
|
|
29
|
+
</option>
|
|
30
|
+
|
|
31
|
+
{projects.map((p) => (
|
|
32
|
+
<option key={p.id} value={p.id}>
|
|
33
|
+
{p.name}
|
|
34
|
+
</option>
|
|
35
|
+
))}
|
|
36
|
+
</select>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default ProjectSelect;
|