Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

158 select projects in user #225

Merged
merged 36 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a396c77
List Users
macano Jan 11, 2025
ef4dc3a
Add Admin flag on User
macano Jan 11, 2025
0902e76
hide users in menu
macano Jan 13, 2025
ac2a849
Update Databases.kt
macano Jan 15, 2025
3745473
authenticate and authorize all the user endpoints
macano Jan 20, 2025
d628321
List Users
macano Jan 11, 2025
35cc0bf
Add Admin flag on User
macano Jan 11, 2025
19be1c7
hide users in menu
macano Jan 13, 2025
3e4de10
show is admin in menu
macano Jan 15, 2025
8a018b4
Merge branch 'display-users' into 158-admin-menu
macano Jan 20, 2025
c298179
Load is admin on user info
macano Jan 20, 2025
c0b056a
Merge branch 'main' into 158-admin-menu
macano Jan 21, 2025
00a3c65
fix type
macano Jan 21, 2025
35b638b
fix test
macano Jan 21, 2025
e812555
Align uuid types
Erikvv Jan 21, 2025
4653fa2
clean code
macano Jan 22, 2025
46988bc
Add log in buttom
macano Jan 22, 2025
b255024
change version
macano Jan 22, 2025
691f237
List projects in the user page
macano Jan 22, 2025
f6d407e
add dropdown to select projects
macano Jan 22, 2025
bb0c79a
Merge branch '158-list-projects-in-user' into 158-select-projects-in-…
macano Jan 22, 2025
182d418
Default values
macano Jan 22, 2025
87a170b
Pass projects instead
macano Jan 22, 2025
3337702
Merge branch '158-list-projects-in-user' into 158-select-projects-in-…
macano Jan 23, 2025
aafc0a5
Pre select projects
macano Jan 23, 2025
e1fa047
Cancel edit
macano Jan 24, 2025
fab0fde
Save Projects
macano Jan 24, 2025
629b54f
List projects in the user page
macano Jan 22, 2025
6f3091e
Pass projects instead
macano Jan 22, 2025
a5306e1
Get User and Projects in single call
macano Jan 27, 2025
de0c4a1
Edit projects
macano Jan 27, 2025
b8a194e
Merge branch '158-list-projects-in-user' into 158-select-projects-in-…
macano Jan 27, 2025
7b5adf3
Use one query to load user and projects
macano Jan 27, 2025
fea5737
Quick show projects in users list
macano Jan 27, 2025
d41cc09
Check admin for all Projects endpoints
macano Jan 28, 2025
d5b1392
Merge branch 'main' into 158-select-projects-in-user
macano Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions frontend/src/admin/projects-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useEffect, FunctionComponent, useState } from "react";

import { MultiSelect } from "primereact/multiselect";
import { Project, projectsFromJson } from "zero-zummon";

type ProjectDropdownProps = {
selectedProjects: Project[];
onChange: (selectedProjects: Project[]) => void;
disabled?: boolean;
};

export const ProjectsDropdown: FunctionComponent<ProjectDropdownProps> = ({
selectedProjects,
onChange,
disabled,
}) => {
const [projects, setProjects] = useState<Project[]>([]);

useEffect(() => {
const fetchProjects = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/all-projects`, {
credentials: "include",
});
if (!response.ok) {
throw new Error(`Failed: ${response.statusText}`)
}

setProjects(projectsFromJson(await response.text()))
} catch (error) {
console.error("Error fetching projects:", error);
}
};

fetchProjects();
}, []);

return (
<div>
<label htmlFor="projects">Update Projects: </label>
<MultiSelect
id="projects"
options={projects.map((project) => ({
label: project.name,
value: project,
}))}
value={selectedProjects}
onChange={(e) => onChange(e.value)}
disabled={disabled}
/>
</div>
);
};
4 changes: 2 additions & 2 deletions frontend/src/admin/use-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const useUsers = (): UseUserReturn => {
if (!response.ok) {
throw new Error(`Could not load users: ${response.status} ${response.statusText}`)
}

setUsers(usersFromJson(await response.text()))
const userData = await response.json()
setUsers(userData)
} catch (error) {
alert((error as Error).message)
} finally {
Expand Down
51 changes: 42 additions & 9 deletions frontend/src/admin/user-form.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import React, { FormEvent, FunctionComponent, useEffect, useState, useRef } from "react";
import { useParams } from "react-router-dom";
import { PrimeReactProvider } from "primereact/api";
import { InputText } from "primereact/inputtext";
import { Button } from "primereact/button";
import { User, Project, projectsFromJson } from "zero-zummon";
import { redirectToLogin } from "./use-users";
import { ProjectsDropdown } from "./projects-dropdown";
import { UserProjectsList } from "./user-projects-list";
import { Toast } from "primereact/toast";

export const UserForm: FunctionComponent = () => {
const {userId} = useParams<{ userId: string }>();
const [user, setUser] = useState<User | null>(null);
const [originalData, setOriginalData] = useState<User | null>(null);
const [selectedProjects, setSelectedProjects] = useState<Project[]>([]);
const [userProjects, setUserProjects] = useState<Project[]>([]);
const msgs = useRef<Toast>(null);

const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);

const handleCancel = () => {
if (originalData) {
setUser(originalData); // Revert to original data
if (originalData) { // Revert to original data
setUser(originalData);
setSelectedProjects(userProjects)

}
setIsEditing(false);
};
Expand All @@ -34,6 +40,11 @@ export const UserForm: FunctionComponent = () => {
} as User));
};

const transformProjects = (projects: any[]): Project[] => {
const jsonString = JSON.stringify(projects);
return projectsFromJson(jsonString);
};

useEffect(() => {
if (userId) {
const fetchUser = async () => {
Expand All @@ -51,7 +62,8 @@ export const UserForm: FunctionComponent = () => {
setUser(userData);
setOriginalData(userData);
setUserProjects(userData.projects)

const formattedProjects = transformProjects(userData.projects)
setSelectedProjects(formattedProjects)
} else {
alert(`Error fetching user: ${response.statusText}`);
}
Expand All @@ -61,7 +73,6 @@ export const UserForm: FunctionComponent = () => {
setLoading(false);
}
};

fetchUser();
} else {
setIsEditing(true);
Expand All @@ -72,6 +83,13 @@ export const UserForm: FunctionComponent = () => {
event.preventDefault();
setLoading(true);
try {
const sendUser = JSON.stringify({
...user,
projects: selectedProjects.map((project) => ({
id: project.id.toString(),
name: project.name,
})),
})
const method = userId ? "PUT" : "POST";
const url = `${import.meta.env.VITE_ZTOR_URL}/users`
const response = await fetch(url, {
Expand All @@ -80,14 +98,21 @@ export const UserForm: FunctionComponent = () => {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(user),
body: sendUser,
});

if (response.status === 401) {
redirectToLogin();
return;
}
if (!response.ok) {
alert(`Error: ${response.statusText}`);
if (response.ok) {
msgs.current?.show([
{ sticky: true, severity: "success", summary: "Success", detail: "User saved successfully.", closable: true },
]);
setUserProjects(selectedProjects);
} else {
msgs.current?.show([
{sticky: true, severity: 'error', summary: 'Error', detail: `Error: ${response.statusText}`, closable: false},
]);
}
} finally {
setIsEditing(false);
Expand All @@ -97,6 +122,7 @@ export const UserForm: FunctionComponent = () => {

return (
<PrimeReactProvider>
<Toast ref={msgs} />
<div style={{ padding: "20px", maxWidth: "500px", margin: "0 auto" }}>
<h3>{userId ? "Edit User" : "Add User"}</h3>
<form
Expand Down Expand Up @@ -133,6 +159,13 @@ export const UserForm: FunctionComponent = () => {
</label>
</div>

<ProjectsDropdown
selectedProjects={selectedProjects}
onChange={setSelectedProjects}
disabled={!isEditing}
/>


<div style={{ display: "flex", justifyContent: "space-between", marginTop: "10px" }}>
{isEditing ? (
<>
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/admin/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import {useUsers} from "./use-users";
import {PrimeReactProvider} from "primereact/api";
import {User} from "zero-zummon"
import {User, Project} from "zero-zummon"

import "primereact/resources/themes/lara-light-cyan/theme.css"
import 'primeicons/primeicons.css'
Expand Down Expand Up @@ -53,6 +53,17 @@ export const Users: FunctionComponent = () => {
</div>
)}
/>
<Column
field="projects"
header="Projects"
body={(user: User) => (
<ul>
{Array.from(user.projects).map((project: Project) => (
<li key={project.id}>{project.name}</li>
))}
</ul>
)}
/>
<Column body={(user: User) => (
<div css={{
display: 'flex',
Expand Down
41 changes: 35 additions & 6 deletions zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.zenmo.zummon.companysurvey.Project
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import java.util.UUID
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.toJavaUuid
Expand Down Expand Up @@ -69,17 +70,45 @@ class UserRepository(
}

@OptIn(ExperimentalUuidApi::class)
fun save(
user: User,
) {
fun save(user: User) {
transaction(db) {
UserTable.upsertReturning() {
UserTable.upsertReturning {
it[id] = user.id.toJavaUuid()
it[UserTable.note] = user.note
it[UserTable.isAdmin] = user.isAdmin
it[note] = user.note
it[isAdmin] = user.isAdmin
}.map {
hydrateUser(it)
}.first()

// db project ids
val currentProjectIds = UserProjectTable
.selectAll()
.where { UserProjectTable.userId eq user.id.toJavaUuid() }
.map { it[UserProjectTable.projectId] }
.toSet()

// coming project ids
val newProjectIds = user.projects.map { it.id }.toSet()

// add and remove projects
val projectsToAdd = newProjectIds - currentProjectIds
val projectsToRemove = currentProjectIds - newProjectIds

// insert new
if (projectsToAdd.isNotEmpty()) {
UserProjectTable.batchInsert(projectsToAdd) { projectId ->
this[UserProjectTable.projectId] = projectId
this[UserProjectTable.userId] = user.id.toJavaUuid()
}
}

// remove old
if (projectsToRemove.isNotEmpty()) {
UserProjectTable.deleteWhere {
(UserProjectTable.userId eq user.id.toJavaUuid()) and
(UserProjectTable.projectId inList projectsToRemove)
}
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ fun Application.configureDatabases(): Database {

get("/users") {
asAdmin {
val users = userRepository.getUsers()
val users = userRepository.getUsersAndProjects()
call.respond(HttpStatusCode.OK, users)
}
}
Expand Down Expand Up @@ -116,6 +116,12 @@ fun Application.configureDatabases(): Database {
}
}

get("/all-projects") {
asAdmin {
call.respond(HttpStatusCode.OK, projectRepository.getProjects())
}
}

get("/projects") {
val userId = call.getUserId()
if (userId == null) {
Expand Down
2 changes: 1 addition & 1 deletion zummon/src/commonMain/kotlin/companysurvey/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ constructor(
val id: Uuid = uuid4(),
val name: String = "",
// Project ID aka Energy Hub ID of Energieke Regio.
val energiekeRegioId: Int?,
val energiekeRegioId: Int? = null,
val buurtCodes: List<String> = emptyList(),
)

Expand Down
Loading