Repository navigation
[CSM Portal Microapp] Build out the Case detail page #1149
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
Merged
Rashmika998
merged 33 commits into
wso2-open-operations:dev-app-csm-portal
from
Hesara2003:feature/csm-microapp-case-detail
Jul 15, 2026
Merged
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
526b844
[CSM Portal Microapp] Build out the Case detail page: lifecycle actio…
Hesara2003 3895715
[CSM Portal Microapp] Fix the single-active-case pause-conflict flow …
Hesara2003 b65d1eb
[CSM Portal Microapp] Fix case detail dialog input behavior
Hesara2003 6b249b8
[CSM Portal Microapp] Add case activity DTOs for the activities/searc…
Hesara2003 4bbbb97
[CSM Portal Microapp] Add CaseAuditEntry model for case activities
Hesara2003 111ad7e
[CSM Portal Microapp] Export the new activity types
Hesara2003 9fb58d3
[CSM Portal Microapp] Add the case activities search endpoint
Hesara2003 642f826
[CSM Portal Microapp] Add a case activities service (field-change aud…
Hesara2003 3d53154
[CSM Portal Microapp] Add a unified, filterable case activity feed
Hesara2003 b2d4654
[CSM Portal Microapp] Wire up the activity feed, icon-based details c…
Hesara2003 0ebba94
[CSM Portal Microapp] Fix the Change state gradient to use the theme'…
Hesara2003 5bed1ae
[CSM Portal Microapp] Replace the comment type toggle with an Interna…
Hesara2003 fe22e92
[CSM Portal Microapp] Rename the attachment picker button to Attachments
Hesara2003 be32f8c
[CSM Portal Microapp] Add an Open button alongside Download for attac…
Hesara2003 9784236
[CSM Portal Microapp] Split AttachmentsField into a picker button + l…
Hesara2003 fb51fcd
[CSM Portal Microapp] Put the Attachments button next to Post in the …
Hesara2003 2a200bf
[CSM Portal Microapp] Make the Change state buttons pill-shaped
Hesara2003 f4e0d6c
[CSM Portal Microapp] Style Change severity as an orange-outlined pil…
Hesara2003 c0243e7
[CSM Portal Microapp] Remove the category filter/sort chips from the …
Hesara2003 c96eb86
[CSM Portal Microapp] Keep only failed attachments after a partial up…
Hesara2003 f5ea431
[CSM Portal Microapp] Fix HTML detection and truncation on comment bo…
Hesara2003 0175db9
[CSM Portal Microapp] Gate comment submit on the selected type's own …
Hesara2003 833513f
[CSM Portal Microapp] Ignore dialog dismissal while the pause mutatio…
Hesara2003 cad2a82
[CSM Portal Microapp] Reset the file input after an oversized-file re…
Hesara2003 0f9d9a0
[CSM Portal Microapp] Normalize both sides of the ongoing-case email …
Hesara2003 c8dc647
[CSM Portal Microapp] Fall back to the default message when unwrappin…
Hesara2003 71e9915
[CSM Portal Microapp] Revert partial state on ongoing-conflict race; …
Hesara2003 6209cf1
[CSM Portal Microapp] Page through the full case activity history
Hesara2003 99f0e84
[CSM Portal Microapp] Drop the Download attachment button that duplic…
Hesara2003 e2b8e82
[CSM Portal Microapp] Remove redundant Fragment wrappers in CaseActio…
Hesara2003 b4240f7
[CSM Portal Microapp] Rename attachment picker buttons to "Upload att…
Hesara2003 6f5b33d
[CSM Portal Microapp] Shrink attachment subtext and space out view/do…
Hesara2003 251936a
[CSM Portal Microapp] Regroup Details tab fields into a single paired…
Hesara2003 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
190 changes: 190 additions & 0 deletions
190
apps/csm-portal/microapp/src/components/case-detail/AttachmentsTab.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| // Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). | ||
| // | ||
| // WSO2 LLC. licenses this file to you under the Apache License, | ||
| // Version 2.0 (the "License"); you may not use this file except | ||
| // in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
|
|
||
| import { Suspense, useState, type ReactNode } from "react"; | ||
| import { useQueryClient, useQueryErrorResetBoundary, useSuspenseQuery } from "@tanstack/react-query"; | ||
| import { Button, IconButton, Skeleton, Stack, Typography } from "@wso2/oxygen-ui"; | ||
| import { Download, Eye, FileText } from "@wso2/oxygen-ui-icons-react"; | ||
| import { attachments as attachmentsService } from "@src/services/attachments"; | ||
| import type { CaseAttachment } from "@src/types"; | ||
| import { ErrorBoundary } from "@components/common/ErrorBoundary"; | ||
| import { ErrorState } from "@components/support/ErrorState"; | ||
| import { AttachmentsField } from "@components/support/AttachmentsField"; | ||
| import { formatBytes, type PendingAttachment } from "@utils/attachments"; | ||
| import { formatDate } from "@utils/dateTime"; | ||
| import { openUrl } from "@components/microapp-bridge"; | ||
| import { Logger } from "@utils/logger"; | ||
|
|
||
| /** | ||
| * Full attachment list + upload for a case — the standalone flow (10 MB cap), distinct from the | ||
| * comment composer's 5 MB inline attach (see CommentComposer.tsx / utils/attachments.ts). | ||
| */ | ||
| export function AttachmentsTab({ caseId }: { caseId: string }) { | ||
| return ( | ||
| <AttachmentsTabErrorBoundary> | ||
| <Suspense fallback={<AttachmentsTabSkeleton />}> | ||
| <AttachmentsTabContent caseId={caseId} /> | ||
| </Suspense> | ||
| </AttachmentsTabErrorBoundary> | ||
| ); | ||
| } | ||
|
|
||
| function AttachmentsTabContent({ caseId }: { caseId: string }) { | ||
| const queryClient = useQueryClient(); | ||
| const { data: caseAttachments } = useSuspenseQuery(attachmentsService.forCase(caseId)); | ||
| const [pending, setPending] = useState<PendingAttachment[]>([]); | ||
| const [isUploading, setIsUploading] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| const handleUpload = async () => { | ||
| if (pending.length === 0) return; | ||
| setIsUploading(true); | ||
| setError(null); | ||
| const results = await Promise.allSettled( | ||
| pending.map((attachment) => | ||
| attachmentsService.create({ | ||
| referenceId: caseId, | ||
| referenceType: "case", | ||
| name: attachment.name, | ||
| type: attachment.type, | ||
| file: attachment.file, | ||
| }), | ||
| ), | ||
| ); | ||
| const failedCount = results.filter((r) => r.status === "rejected").length; | ||
| if (failedCount > 0) { | ||
| Logger.warn(`${failedCount} attachment(s) failed to upload to case ${caseId}`); | ||
| setError( | ||
| failedCount === pending.length | ||
| ? "Could not upload the file(s). Please try again." | ||
| : `${failedCount} file(s) failed to upload.`, | ||
| ); | ||
| } | ||
| // Keep only the files that actually failed, so a partial (or total) failure leaves them | ||
| // selected for retry instead of silently dropping them along with the successful ones. | ||
| // Indexes line up with `results` since the field is disabled (via isUploading) for the | ||
| // duration of this upload, so `pending` can't have changed underneath us. | ||
| setPending(pending.filter((_, i) => results[i].status === "rejected")); | ||
| setIsUploading(false); | ||
| void queryClient.invalidateQueries({ queryKey: ["case", caseId, "attachments"] }); | ||
| }; | ||
|
|
||
| return ( | ||
| <Stack gap={2}> | ||
| <AttachmentsField attachments={pending} onChange={setPending} disabled={isUploading} /> | ||
|
|
||
| {pending.length > 0 && ( | ||
| <Button | ||
| variant="contained" | ||
| size="small" | ||
| disabled={isUploading} | ||
| onClick={() => void handleUpload()} | ||
| sx={{ alignSelf: "end" }} | ||
| > | ||
| Upload | ||
| </Button> | ||
| )} | ||
|
|
||
| {error && ( | ||
| <Typography variant="caption" color="error.main"> | ||
| {error} | ||
| </Typography> | ||
| )} | ||
|
|
||
| {caseAttachments.length === 0 ? ( | ||
| <Typography variant="body2" color="text.secondary"> | ||
| No attachments on this case. | ||
| </Typography> | ||
| ) : ( | ||
| <Stack gap={1}> | ||
| {caseAttachments.map((attachment) => ( | ||
| <AttachmentRow key={attachment.id} attachment={attachment} /> | ||
| ))} | ||
| </Stack> | ||
| )} | ||
| </Stack> | ||
| ); | ||
| } | ||
|
|
||
| function AttachmentRow({ attachment }: { attachment: CaseAttachment }) { | ||
| return ( | ||
| <Stack | ||
| direction="row" | ||
| alignItems="center" | ||
| justifyContent="space-between" | ||
| gap={1} | ||
| sx={{ p: 1, border: "1px solid", borderColor: "divider", borderRadius: 1 }} | ||
| > | ||
| <Stack direction="row" alignItems="center" gap={1} sx={{ minWidth: 0 }}> | ||
| <FileText size={16} /> | ||
| <Stack sx={{ minWidth: 0 }}> | ||
| <Typography variant="body2" noWrap> | ||
| {attachment.name} | ||
| </Typography> | ||
| <Typography variant="caption" color="text.secondary" noWrap> | ||
| {formatBytes(attachment.sizeBytes)} · {attachment.createdBy} · {formatDate(attachment.createdOn)} | ||
| </Typography> | ||
| </Stack> | ||
| </Stack> | ||
| {attachment.downloadUrl && ( | ||
| <Stack direction="row" gap={0.5} sx={{ flexShrink: 0 }}> | ||
| <IconButton | ||
| size="small" | ||
| aria-label={`Open ${attachment.name}`} | ||
| onClick={() => openUrl({ url: attachment.downloadUrl as string, presentationStyle: "fullScreen" })} | ||
| > | ||
| <Eye size={16} /> | ||
| </IconButton> | ||
| <IconButton | ||
| size="small" | ||
| aria-label={`Download ${attachment.name}`} | ||
| onClick={() => openUrl({ url: attachment.downloadUrl as string, presentationStyle: "fullScreen" })} | ||
| > | ||
| <Download size={16} /> | ||
| </IconButton> | ||
| </Stack> | ||
| )} | ||
| </Stack> | ||
| ); | ||
| } | ||
|
|
||
| function AttachmentsTabSkeleton() { | ||
| return ( | ||
| <Stack gap={1}> | ||
| <Skeleton variant="rounded" height={56} /> | ||
| <Skeleton variant="rounded" height={56} /> | ||
| </Stack> | ||
| ); | ||
| } | ||
|
|
||
| function AttachmentsTabErrorBoundary({ children }: { children: ReactNode }) { | ||
| const { reset } = useQueryErrorResetBoundary(); | ||
|
|
||
| return ( | ||
| <ErrorBoundary | ||
| fallback={(_error, resetErrorBoundary) => ( | ||
| <ErrorState | ||
| onRetry={() => { | ||
| reset(); | ||
| resetErrorBoundary(); | ||
| }} | ||
| /> | ||
| )} | ||
| > | ||
| {children} | ||
| </ErrorBoundary> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.