Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/core/utils/eventBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import mitt from "mitt"
type AppEvents = {
taskLogUpdated: void
announcementCreated: void
milestoneTasksUpdated: void
// Add more events here as needed:
// modalClosed: void
// userLoggedIn: { userId: number }
Expand Down
33 changes: 20 additions & 13 deletions src/milestones/components/MilestoneSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { eventBus } from "src/core/utils/eventBus"
import { useQuery } from "@blitzjs/rpc"
import React, { useEffect, useRef } from "react"
import "react-circular-progressbar/dist/styles.css"
import { Milestone } from "@prisma/client"
import { completedTaskPercentage } from "src/widgets/utils/completedTaskPercentage"
Expand All @@ -18,25 +20,20 @@ interface MilestoneSummaryProps {

export const MilestoneSummary: React.FC<MilestoneSummaryProps> = ({ milestone, projectId }) => {
// Get tasks
const [{ tasks }] = useQuery(getTasks, {
include: {
roles: true,
},
where: {
projectId: projectId,
milestoneId: milestone.id,
},
const taskQuery = useQuery(getTasks, {
include: { roles: true },
where: { projectId: projectId, milestoneId: milestone.id },
})
const [{ tasks }, { refetch: refetchTasks }] = taskQuery

// get taskLogs for those tasks
const [fetchedTaskLogs] = useQuery(getTaskLogs, {
const taskLogsQuery = useQuery(getTaskLogs, {
where: {
taskId: { in: tasks.map((task) => task.id) },
},
include: {
task: true,
},
}) as unknown as TaskLogWithTask[]
include: { task: true },
})
const [fetchedTaskLogs, { refetch: refetchLogs }] = taskLogsQuery as any

// Cast and handle the possibility of `undefined`
const taskLogs: TaskLogWithTask[] = (fetchedTaskLogs ?? []) as TaskLogWithTask[]
Expand All @@ -49,6 +46,16 @@ export const MilestoneSummary: React.FC<MilestoneSummaryProps> = ({ milestone, p
const taskPercent = completedTaskPercentage(tasks)
const rolePercent = completedRolePercentage(tasks)

// Listen for custom event to refetch tasks/logs
useEffect(() => {
const handler = () => {
void refetchTasks()
void refetchLogs()
}
eventBus.on("milestoneTasksUpdated", handler)
return () => eventBus.off("milestoneTasksUpdated", handler)
}, [refetchTasks, refetchLogs])

return (
<CollapseCard title="Milestone Statistics" className="mt-4">
<div className="stats flex justify-between items-center gap-4 bg-base-300 text-lg font-bold">
Expand Down
2 changes: 2 additions & 0 deletions src/milestones/components/UpdateTasksMilestone.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from "react"
import { eventBus } from "src/core/utils/eventBus"
import Modal from "src/core/components/Modal"
import CheckboxFieldTable from "src/core/components/fields/CheckboxFieldTable"
import Form from "src/core/components/fields/Form"
Expand Down Expand Up @@ -40,6 +41,7 @@ const UpdateTasksMilestone: React.FC<UpdateTasksMilestoneProps> = ({
})

onTasksUpdated()
eventBus.emit("milestoneTasksUpdated")
onClose()
}

Expand Down
7 changes: 5 additions & 2 deletions src/pages/projects/[projectId]/milestones/[milestoneId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ const ShowMilestonePage = () => {
},
},
})
const milestoneTasks = tasks.filter((task) => task.milestoneId === null)
const milestoneTasks = tasks.filter((task) => task.milestoneId === milestone.id)
const updateTasks = tasks.filter(
(task) => task.milestoneId === milestone.id || task.milestoneId === null
)
const processedTasks = processProjectTasks(milestoneTasks)

return (
Expand Down Expand Up @@ -102,7 +105,7 @@ const ShowMilestonePage = () => {
open={isModalOpen}
onClose={closeModal}
onTasksUpdated={refetchTasks}
tasks={milestoneTasks}
tasks={updateTasks}
/>

<button type="button" className="btn btn-warning" onClick={handleDelete}>
Expand Down
4 changes: 3 additions & 1 deletion src/pages/roles/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { NewRole } from "src/roles/components/NewRole"
import { InformationCircleIcon } from "@heroicons/react/24/outline"
import { Tooltip } from "react-tooltip"
import Card from "src/core/components/Card"
import { DefaultRoles } from "src/roles/components/DefaultRoles"

const RoleBuilderPage = () => {
const currentUser = useCurrentUser()
Expand Down Expand Up @@ -38,8 +39,9 @@ const RoleBuilderPage = () => {
/>
</h1>

<div className="flex justify-center mt-4 mb-2">
<div className="flex justify-center mt-4 mb-2 gap-2">
<NewRole taxonomyList={taxonomyList} onRolesChanged={refetch} />
<DefaultRoles onRolesChanged={refetch} />
</div>
<Card title={""}>
<Suspense fallback={<div>Loading...</div>}>
Expand Down
11 changes: 9 additions & 2 deletions src/projects/components/ProjectSchemaInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ export const ProjectSchemaInput = ({
toast.success("Default form has been successfully created!")

// Ensure versions exists and has at least one item
const formVersionId = newFormVersion.versions?.[0]?.id
const { data: updatedForms } = await refetchForms()

const allVersions = (updatedForms ?? []).flatMap((form) => form.formVersion ?? [])
const sortedVersions = allVersions.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
const formVersionId = sortedVersions[0]?.id

if (formVersionId) {
onDefaultFormCreated(formVersionId)
} else {
Expand All @@ -64,7 +71,7 @@ export const ProjectSchemaInput = ({
}
}

const [userForms] = useQuery(getForms, {
const [userForms, { refetch: refetchForms }] = useQuery(getForms, {
where: { userId: { in: userId }, archived: false },
})

Expand Down
98 changes: 98 additions & 0 deletions src/roles/components/DefaultRoles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useMutation } from "@blitzjs/rpc"
import { useCurrentUser } from "src/users/hooks/useCurrentUser"
import importDefaultRoles from "../mutations/importDefaultRoles"
import { useState } from "react"
import toast from "react-hot-toast"
import Modal from "src/core/components/Modal"
import { Form } from "react-final-form"
import RadioFieldTable from "src/core/components/fields/RadioFieldTable"
import { defaultRoleTemplates } from "../templates/defaultRoles"
import CheckboxFieldTable from "src/core/components/fields/CheckboxFieldTable"

interface DefaultRolesProps {
onRolesChanged?: () => void
}

export const DefaultRoles = ({ onRolesChanged }: DefaultRolesProps) => {
const currentUser = useCurrentUser()
const [importDefaultRolesMutation] = useMutation(importDefaultRoles)
const [openImportModal, setOpenImportModal] = useState(false)

const handleToggleImportModal = () => {
setOpenImportModal((prev) => !prev)
}

const options = defaultRoleTemplates.map((template, index) => ({
id: index,
label: template.label,
}))

const extraData = defaultRoleTemplates.map((template) => ({
link: template.link,
}))

const extraColumns = [
{
id: "link",
header: "Link",
accessorKey: "link",
cell: (info) => (
<a href={info.getValue()} target="_blank" rel="noopener noreferrer" className="link">
{info.getValue()}
</a>
),
},
]

return (
<>
<button type="button" className="btn btn-secondary" onClick={handleToggleImportModal}>
Import Default Roles
</button>

<Modal open={openImportModal} size="w-1/3 max-w-1/2">
<Form
onSubmit={async (values) => {
const selectedIndex = values.defaultRoleForm?.[0]
if (typeof selectedIndex !== "number") return
const system = [defaultRoleTemplates[selectedIndex]!.id]
try {
await importDefaultRolesMutation({ system, userId: currentUser!.id })
toast.success("Default roles imported!")
setOpenImportModal(false)
if (onRolesChanged) onRolesChanged()
} catch (error: any) {
toast.error("Failed to import default roles.")
console.error(error)
}
}}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<h1 className="mb-4 text-2xl font-bold text-center">Import Default Roles</h1>
<div className="flex flex-col gap-2">
<CheckboxFieldTable
name="defaultRoleForm"
options={options}
extraColumns={extraColumns}
extraData={extraData}
/>
</div>
<div className="mt-4 flex justify-end gap-2">
<button type="submit" className="btn btn-primary">
Import Selected
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleToggleImportModal}
>
Cancel
</button>
</div>
</form>
)}
/>
</Modal>
</>
)
}
42 changes: 42 additions & 0 deletions src/roles/mutations/importDefaultRoles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { resolver } from "@blitzjs/rpc"
import db from "db"
import { z } from "zod"
import { defaultRoleTemplates } from "../templates/defaultRoles"

const roleSystemIds = defaultRoleTemplates.map((r) => r.id) as [string, ...string[]]

const ImportRolesSchema = z.object({
system: z.array(z.enum(roleSystemIds)),
userId: z.number(),
})

export default resolver.pipe(
resolver.zod(ImportRolesSchema),
resolver.authorize(),
async ({ system, userId }) => {
// system is now an array of ids
const allTemplates = system.flatMap((sysId) => {
const matched = defaultRoleTemplates.find((r) => r.id === sysId)
if (!matched) throw new Error(`Unknown role system: ${sysId}`)
return matched.roles.map((role) => ({
...role,
taxonomy: matched.label,
}))
})

const createdRoles = await db.$transaction(
allTemplates.map((role) =>
db.role.create({
data: {
name: role.name,
description: role.description,
taxonomy: role.taxonomy,
userId,
},
})
)
)

return createdRoles
}
)
6 changes: 6 additions & 0 deletions src/roles/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod"
import { defaultRoleTemplates } from "./templates/defaultRoles"

export const CreateRoleSchema = z.object({
userId: z.number(),
Expand Down Expand Up @@ -26,3 +27,8 @@ export const RoleFormSchema = z.object({
export const RoleIdsFormSchema = z.object({
rolesId: z.array(z.number()).optional().nullable(),
})

export const ImportRolesSchema = z.object({
system: z.array(z.enum(defaultRoleTemplates.map((r) => r.id) as [string, ...string[]])),
userId: z.number(),
})
Loading