diff --git a/packages/backend/scripts/src/modules/rest/comments/comment.service.ts b/packages/backend/scripts/src/modules/rest/comments/comment.service.ts index b3bce2c0..acde6435 100644 --- a/packages/backend/scripts/src/modules/rest/comments/comment.service.ts +++ b/packages/backend/scripts/src/modules/rest/comments/comment.service.ts @@ -53,8 +53,10 @@ export class CommentService implements CommentREST { const product = await Database.get().productRepository.findOneBy({ productId }) product.updated = comment.updated await Database.get().productRepository.save(product) + // Find milestone + const milestones = issue.milestoneId ? await Database.get().milestoneRepository.findBy({ milestoneId: issue.milestoneId }) : [] // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment] }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment], milestones }) // Notify changes this.notifyComment(product, issue, comment, 'Comment notification (add)') // Return comment @@ -80,8 +82,10 @@ export class CommentService implements CommentREST { const product = await Database.get().productRepository.findOneBy({ productId }) product.updated = comment.updated await Database.get().productRepository.save(product) + // Find milestone + const milestones = issue.milestoneId ? await Database.get().milestoneRepository.findBy({ milestoneId: issue.milestoneId }) : [] // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment] }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment], milestones }) // Notify changes this.notifyComment(product, issue, comment, 'Comment notification (update)') // Return comment @@ -103,8 +107,10 @@ export class CommentService implements CommentREST { const product = await Database.get().productRepository.findOneBy({ productId }) product.updated = comment.updated await Database.get().productRepository.save(product) + // Find milestone + const milestones = issue.milestoneId ? await Database.get().milestoneRepository.findBy({ milestoneId: issue.milestoneId }) : [] // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment] }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments: [comment], milestones }) // Return comment return convertComment(comment) } diff --git a/packages/backend/scripts/src/modules/rest/issues/issue.service.ts b/packages/backend/scripts/src/modules/rest/issues/issue.service.ts index 7b21ed9d..39fba4a9 100644 --- a/packages/backend/scripts/src/modules/rest/issues/issue.service.ts +++ b/packages/backend/scripts/src/modules/rest/issues/issue.service.ts @@ -6,7 +6,7 @@ import shortid from 'shortid' import { IsNull } from 'typeorm' import { IssueCreate, IssueREST, IssueRead, IssueUpdate, ProductRead } from 'productboard-common' -import { Database, convertIssue } from 'productboard-database' +import { Database, MilestoneEntity, convertIssue } from 'productboard-database' import { emitProductMessage } from '../../../functions/emit' import { TRANSPORTER } from '../../../functions/mail' @@ -41,8 +41,10 @@ export class IssueService implements IssueREST { const product = await Database.get().productRepository.findOneBy({ productId }) product.updated = issue.updated await Database.get().productRepository.save(product) + // Find milestone + const milestones = data.milestoneId ? await Database.get().milestoneRepository.findBy({ milestoneId: data.milestoneId }) : [] // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue] }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], milestones }) // Notify changes this.notifyIssue(product, issue, 'Issue notification (add)') // Return issue @@ -55,8 +57,19 @@ export class IssueService implements IssueREST { } async updateIssue(productId: string, issueId: string, data: IssueUpdate): Promise { - // Update issue + // Find issue const issue = await Database.get().issueRepository.findOneByOrFail({ productId, issueId }) + // Find milestone + const milestones: MilestoneEntity[] = [] + if (issue.milestoneId != data.milestoneId) { + if (issue.milestoneId) { + milestones.push(await Database.get().milestoneRepository.findOneBy({ milestoneId: issue.milestoneId })) + } + if (data.milestoneId) { + milestones.push(await Database.get().milestoneRepository.findOneBy({ milestoneId: data.milestoneId })) + } + } + // Update issue issue.updated = Date.now() issue.assignedUserIds = data.assignedUserIds issue.label = data.label @@ -67,7 +80,7 @@ export class IssueService implements IssueREST { product.updated = issue.updated await Database.get().productRepository.save(product) // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue] }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], milestones }) // Notify changes this.notifyIssue(product, issue, 'Issue notification (update)') // Return issue @@ -91,8 +104,10 @@ export class IssueService implements IssueREST { comment.updated = issue.updated await Database.get().commentRepository.save(comment) } + // Find milestone + const milestones = issue.milestoneId ? await Database.get().milestoneRepository.findBy({ milestoneId: issue.milestoneId }) : [] // Emit changes - emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments }) + emitProductMessage(productId, { type: 'patch', products: [product], issues: [issue], comments, milestones }) // Return issue return convertIssue(issue) } diff --git a/packages/common/src/data/product.ts b/packages/common/src/data/product.ts index 6759c6fe..c2e348fe 100644 --- a/packages/common/src/data/product.ts +++ b/packages/common/src/data/product.ts @@ -34,7 +34,9 @@ export class ProductRead extends ProductCreate { @ApiProperty() closedIssueCount?: number @ApiProperty() - milestoneCount?: number + openMilestoneCount?: number + @ApiProperty() + closedMilestoneCount?: number @ApiProperty() memberCount?: number } \ No newline at end of file diff --git a/packages/database/src/convert.ts b/packages/database/src/convert.ts index 64c78135..cfbda1e1 100644 --- a/packages/database/src/convert.ts +++ b/packages/database/src/convert.ts @@ -27,12 +27,15 @@ export async function convertUser(user: UserEntity, full: boolean): Promise { + + const productId = product.productId - const versionCount = await Database.get().versionRepository.countBy({ product, deleted: IsNull() }) - const openIssueCount = await Database.get().issueRepository.countBy({ product, state: "open", deleted: IsNull() }) - const closedIssueCount = await Database.get().issueRepository.countBy({ product, state: "closed", deleted: IsNull() }) - const milestoneCount = await Database.get().milestoneRepository.countBy({ product, deleted: IsNull() }) - const memberCount = await Database.get().memberRepository.countBy({ product, deleted: IsNull() }) + const versionCount = await Database.get().versionRepository.countBy({ productId, deleted: IsNull() }) + const openIssueCount = await Database.get().issueRepository.countBy({ productId, state: "open", deleted: IsNull() }) + const closedIssueCount = await Database.get().issueRepository.countBy({ productId, state: "closed", deleted: IsNull() }) + const openMilestoneCount = await Database.get().milestoneRepository.countBy({ productId, deleted: IsNull() }) + const closedMilestoneCount = 0 // TODO compute closed milestone count + const memberCount = await Database.get().memberRepository.countBy({ productId, deleted: IsNull() }) return { userId: product.userId, @@ -46,7 +49,8 @@ export async function convertProduct(product: ProductEntity): Promise { + + const issueId = issue.issueId - const commentCount = await Database.get().commentRepository.countBy({ issue, deleted: IsNull() }) + const commentCount = await Database.get().commentRepository.countBy({ issueId, deleted: IsNull() }) const attachmentCount = 0 // TODO compute attachment count const partCount = 0 // TODO compute part count @@ -122,8 +128,10 @@ export async function convertAttachment(attachment: AttachmentEntity): Promise { - const openIssueCount = await Database.get().issueRepository.countBy({ milestone, state: "open", deleted: IsNull() }) - const closedIssueCount = await Database.get().issueRepository.countBy({ milestone, state: "closed", deleted: IsNull() }) + const milestoneId = milestone.milestoneId + + const openIssueCount = await Database.get().issueRepository.countBy({ milestoneId, state: "open", deleted: IsNull() }) + const closedIssueCount = await Database.get().issueRepository.countBy({ milestoneId, state: "closed", deleted: IsNull() }) return { userId: milestone.userId, diff --git a/packages/frontend/src/scripts/clients/cache.ts b/packages/frontend/src/scripts/clients/cache.ts index 44ac66e9..e14d93b1 100644 --- a/packages/frontend/src/scripts/clients/cache.ts +++ b/packages/frontend/src/scripts/clients/cache.ts @@ -71,7 +71,7 @@ function notify(callbacks: Index[]>, id: string, value: T) { } } function update(entityCache: Index, childCache: Index>, entityCallbacks: Index[]>, childCallbacks: Index[]>, entityId: string, parentId: string, value: T) { - if (!(entityId in entityCache) || entityCache[entityId].updated < value.updated) { + if (!(entityId in entityCache) || entityCache[entityId].updated <= value.updated) { // Update caches entityCache[entityId] = value if (parentId) { diff --git a/packages/frontend/src/scripts/clients/mqtt.ts b/packages/frontend/src/scripts/clients/mqtt.ts index dddf69bc..df009185 100644 --- a/packages/frontend/src/scripts/clients/mqtt.ts +++ b/packages/frontend/src/scripts/clients/mqtt.ts @@ -32,6 +32,7 @@ function init() { // Forward message to handlers client.on('message', (topic, payload) => { const object = JSON.parse(payload.toString()) + console.log(object) for (const pattern in handlers) { if (matches(pattern, topic)) { for (const handler of handlers[pattern]) { diff --git a/packages/frontend/src/scripts/components/links/IssuesLink.tsx b/packages/frontend/src/scripts/components/links/IssuesLink.tsx index 2131bfbc..8da834d8 100644 --- a/packages/frontend/src/scripts/components/links/IssuesLink.tsx +++ b/packages/frontend/src/scripts/components/links/IssuesLink.tsx @@ -4,7 +4,6 @@ import { NavLink, useLocation } from 'react-router-dom' import { ProductRead } from 'productboard-common' import { useAsyncHistory } from '../../hooks/history' -import { useIssues } from '../../hooks/list' import { PRODUCTS_4 } from '../../pattern' import IssueIcon from '/src/images/issue.png' @@ -14,10 +13,6 @@ export const IssuesLink = (props: {product: ProductRead}) => { const { pathname } = useLocation() const { go, goBack, replace } = useAsyncHistory() - // HOOKS - - const issues = useIssues(props.product.productId, undefined, 'open') - // FUNCTIONS async function handleClick(event: React.UIEvent) { @@ -42,7 +37,7 @@ export const IssuesLink = (props: {product: ProductRead}) => { Issues - {issues ? issues.length : '?'} + {props.product.openIssueCount} ) diff --git a/packages/frontend/src/scripts/components/links/MembersLink.tsx b/packages/frontend/src/scripts/components/links/MembersLink.tsx index 54856326..49eee811 100644 --- a/packages/frontend/src/scripts/components/links/MembersLink.tsx +++ b/packages/frontend/src/scripts/components/links/MembersLink.tsx @@ -4,7 +4,6 @@ import { NavLink, useLocation } from 'react-router-dom' import { ProductRead } from 'productboard-common' import { useAsyncHistory } from '../../hooks/history' -import { useMembers } from '../../hooks/list' import { PRODUCTS_4 } from '../../pattern' import MemberIcon from '/src/images/user.png' @@ -14,10 +13,6 @@ export const MembersLink = (props: {product: ProductRead}) => { const { pathname } = useLocation() const { go, goBack, replace } = useAsyncHistory() - // HOOKS - - const members = useMembers(props.product.productId) - // FUNCTIONS async function handleClick(event: React.UIEvent) { @@ -42,7 +37,7 @@ export const MembersLink = (props: {product: ProductRead}) => { Members - {members ? members.length : '?'} + {props.product.memberCount} ) diff --git a/packages/frontend/src/scripts/components/links/MilestonesLink.tsx b/packages/frontend/src/scripts/components/links/MilestonesLink.tsx index 161edab0..174dde36 100644 --- a/packages/frontend/src/scripts/components/links/MilestonesLink.tsx +++ b/packages/frontend/src/scripts/components/links/MilestonesLink.tsx @@ -4,7 +4,6 @@ import { NavLink, useLocation } from 'react-router-dom' import { ProductRead } from 'productboard-common' import { useAsyncHistory } from '../../hooks/history' -import { useMilestones } from '../../hooks/list' import { PRODUCTS_4 } from '../../pattern' import MilestoneIcon from '/src/images/milestone.png' @@ -14,10 +13,6 @@ export const MilestonesLink = (props: {product: ProductRead}) => { const { pathname } = useLocation() const { go, goBack, replace } = useAsyncHistory() - // HOOKS - - const milestones = useMilestones(props.product.productId) - // FUNCTIONS async function handleClick(event: React.UIEvent) { @@ -42,7 +37,7 @@ export const MilestonesLink = (props: {product: ProductRead}) => { Milestones - {milestones ? milestones.length : '?'} + {props.product.openMilestoneCount} ) diff --git a/packages/frontend/src/scripts/components/links/VersionsLink.tsx b/packages/frontend/src/scripts/components/links/VersionsLink.tsx index fa9084ca..be5373e1 100644 --- a/packages/frontend/src/scripts/components/links/VersionsLink.tsx +++ b/packages/frontend/src/scripts/components/links/VersionsLink.tsx @@ -4,7 +4,6 @@ import { NavLink, useLocation } from 'react-router-dom' import { ProductRead } from 'productboard-common' import { useAsyncHistory } from '../../hooks/history' -import { useVersions } from '../../hooks/list' import { PRODUCTS_4 } from '../../pattern' import VersionIcon from '/src/images/version.png' @@ -14,10 +13,6 @@ export const VersionsLink = (props: {product: ProductRead}) => { const { pathname } = useLocation() const { go, goBack, replace } = useAsyncHistory() - // HOOKS - - const versions = useVersions(props.product.productId) - // FUNCTIONS async function handleClick(event: React.UIEvent) { @@ -42,7 +37,7 @@ export const VersionsLink = (props: {product: ProductRead}) => { Versions - {versions ? versions.length : '?'} + {props.product.versionCount} ) diff --git a/packages/frontend/src/scripts/components/views/Product.tsx b/packages/frontend/src/scripts/components/views/Product.tsx index 80691a23..88e0ed93 100644 --- a/packages/frontend/src/scripts/components/views/Product.tsx +++ b/packages/frontend/src/scripts/components/views/Product.tsx @@ -14,9 +14,6 @@ import { LegalFooter } from '../snippets/LegalFooter' import { Column, Table } from '../widgets/Table' import { ProductImageWidget } from '../widgets/ProductImage' import { ProductUserPicture } from '../values/ProductUserPicture' -import { MemberCount } from '../counts/Members' -import { IssueCount } from '../counts/Issues' -import { VersionCount } from '../counts/Versions' import { LoadingView } from './Loading' import ProductIcon from '/src/images/product.png' @@ -74,17 +71,17 @@ export const ProductView = () => { ) }, { label: 'Versions', class: 'center', content: product => ( - + {product.versionCount} ) }, { label: 'Issues', class: 'center', content: product => ( - + {product.openIssueCount} ) }, { label: 'Members', class: 'center', content: product => ( - + {product.memberCount} ) }, { label: '🧑', class: 'center', content: product => ( diff --git a/packages/frontend/src/scripts/components/views/ProductIssue.tsx b/packages/frontend/src/scripts/components/views/ProductIssue.tsx index b807ffe2..e38c99fc 100644 --- a/packages/frontend/src/scripts/components/views/ProductIssue.tsx +++ b/packages/frontend/src/scripts/components/views/ProductIssue.tsx @@ -10,8 +10,6 @@ import { UserContext } from '../../contexts/User' import { useProduct } from '../../hooks/entity' import { useAsyncHistory } from '../../hooks/history' import { useIssues, useMembers } from '../../hooks/list' -import { CommentCount } from '../counts/Comments' -import { IssueCount } from '../counts/Issues' import { PartCount } from '../counts/Parts' import { LegalFooter } from '../snippets/LegalFooter' import { ProductFooter, ProductFooterItem } from '../snippets/ProductFooter' @@ -114,7 +112,7 @@ export const ProductIssueView = () => { ) }, { label: 'Comments', class: 'center nowrap', content: issue => ( - + {issue.commentCount} ) }, { label: 'Parts', class: 'center nowrap', content: issue => ( @@ -161,10 +159,10 @@ export const ProductIssueView = () => { )} - Open issues + Open issues { product.openIssueCount } - Closed issues + Closed issues { product.closedIssueCount } { issues.filter(issue => issue.state == state).length == 0 ? ( diff --git a/packages/frontend/src/scripts/components/views/ProductMilestone.tsx b/packages/frontend/src/scripts/components/views/ProductMilestone.tsx index d55afa0e..ceab4a21 100644 --- a/packages/frontend/src/scripts/components/views/ProductMilestone.tsx +++ b/packages/frontend/src/scripts/components/views/ProductMilestone.tsx @@ -10,7 +10,6 @@ import { UserContext } from '../../contexts/User' import { useProduct } from '../../hooks/entity' import { useAsyncHistory } from '../../hooks/history' import { useMilestones, useMembers } from '../../hooks/list' -import { IssueCount } from '../counts/Issues' import { formatDateTime } from '../../functions/time' import { LegalFooter } from '../snippets/LegalFooter' import { ProductFooter, ProductFooterItem } from '../snippets/ProductFooter' @@ -78,16 +77,16 @@ export const ProductMilestoneView = () => { ) }, { label: 'Open', class: 'center', content: milestone => ( - + {milestone.openIssueCount} ) }, { label: 'Closed', class: 'center', content: milestone => ( - + {milestone.closedIssueCount} ) }, { label: 'Progress', class: 'center', content: milestone => ( - + ) }, { label: '🛠️', class: 'center', content: milestone => ( deleteMilestone(event, milestone)}> diff --git a/packages/frontend/src/scripts/components/views/ProductMilestoneIssue.tsx b/packages/frontend/src/scripts/components/views/ProductMilestoneIssue.tsx index b65f247b..fe231220 100644 --- a/packages/frontend/src/scripts/components/views/ProductMilestoneIssue.tsx +++ b/packages/frontend/src/scripts/components/views/ProductMilestoneIssue.tsx @@ -13,8 +13,6 @@ import { useMembers, useIssues } from '../../hooks/list' import { useIssuesComments } from '../../hooks/map' import { calculateActual } from '../../functions/burndown' import { formatDateTime } from '../../functions/time' -import { CommentCount } from '../counts/Comments' -import { IssueCount } from '../counts/Issues' import { PartCount } from '../counts/Parts' import { LegalFooter } from '../snippets/LegalFooter' import { ProductFooter, ProductFooterItem } from '../snippets/ProductFooter' @@ -148,7 +146,7 @@ export const ProductMilestoneIssueView = () => { ) }, { label: 'Comments', class: 'center nowrap', content: issue => ( - + {issue.commentCount} ) }, { label: 'Parts', class: 'center nowrap', content: issue => ( @@ -230,10 +228,10 @@ export const ProductMilestoneIssueView = () => { )} - Open issues + Open issues { milestone.openIssueCount } - Closed issues + Closed issues { milestone.closedIssueCount } { issues.filter(issue => issue.state == state).length == 0 ? ( diff --git a/packages/frontend/src/scripts/components/widgets/MilestoneProgress.tsx b/packages/frontend/src/scripts/components/widgets/MilestoneProgress.tsx index eed4b8c4..408faa55 100644 --- a/packages/frontend/src/scripts/components/widgets/MilestoneProgress.tsx +++ b/packages/frontend/src/scripts/components/widgets/MilestoneProgress.tsx @@ -1,27 +1,24 @@ import * as React from 'react' -import { useIssues } from '../../hooks/list' +import { MilestoneRead } from 'productboard-common' -export const MilestoneProgressWidget = (props: { productId: string, milestoneId: string }) => { - const open = useIssues(props.productId, props.milestoneId, 'open') - const closed = useIssues(props.productId, props.milestoneId, 'closed') +export const MilestoneProgressWidget = (props: { milestone: MilestoneRead }) => { + + const open = props.milestone.openIssueCount + const closed = props.milestone.closedIssueCount function width() { - const total = open.length + closed.length + const total = open + closed if (total > 0) { - return Math.floor(closed.length / total * 100) + return Math.floor(closed / total * 100) } else { return 0 } } return ( - open && closed ? ( -
-
-
- ) : ( - <>? - ) +
+
+
) } \ No newline at end of file