@openneuro/app 4.30.2 → 4.31.0

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.
Files changed (58) hide show
  1. package/package.json +5 -5
  2. package/src/assets/activity-icon.png +0 -0
  3. package/src/assets/icon-archived.png +0 -0
  4. package/src/assets/icon-saved.png +0 -0
  5. package/src/assets/icon-unread.png +0 -0
  6. package/src/client.jsx +1 -1
  7. package/src/scripts/datalad/dataset/dataset-query-fragments.js +4 -14
  8. package/src/scripts/dataset/__tests__/__snapshots__/snapshot-container.spec.tsx.snap +4 -304
  9. package/src/scripts/dataset/components/ValidationBlock.tsx +13 -15
  10. package/src/scripts/dataset/components/__tests__/ValidationBlock.spec.tsx +2 -0
  11. package/src/scripts/dataset/draft-container.tsx +2 -1
  12. package/src/scripts/dataset/files/__tests__/__snapshots__/file-tree.spec.jsx.snap +1 -9
  13. package/src/scripts/dataset/fragments/__tests__/{dataset-alert-draft.spec.tsx → dataset-alert.spec.tsx} +33 -1
  14. package/src/scripts/dataset/fragments/{dataset-alert-draft.tsx → dataset-alert.tsx} +30 -18
  15. package/src/scripts/dataset/routes/delete-page.tsx +72 -39
  16. package/src/scripts/dataset/routes/snapshot.tsx +23 -17
  17. package/src/scripts/dataset/routes/tab-routes-draft.tsx +5 -2
  18. package/src/scripts/dataset/snapshot-container.tsx +11 -0
  19. package/src/scripts/search/__tests__/search-params-ctx.spec.tsx +3 -0
  20. package/src/scripts/search/initial-search-params.tsx +2 -0
  21. package/src/scripts/search/inputs/__tests__/nihselect.spec.tsx +36 -0
  22. package/src/scripts/search/inputs/index.ts +2 -0
  23. package/src/scripts/search/inputs/nih-select.tsx +63 -0
  24. package/src/scripts/search/search-container.tsx +20 -12
  25. package/src/scripts/search/search-params-ctx.tsx +2 -0
  26. package/src/scripts/search/search-routes.tsx +14 -6
  27. package/src/scripts/search/use-search-results.tsx +15 -0
  28. package/src/scripts/types/user-types.ts +72 -0
  29. package/src/scripts/uploader/upload-issues.tsx +2 -2
  30. package/src/scripts/users/__tests__/datasest-card.spec.tsx +201 -0
  31. package/src/scripts/users/__tests__/user-card.spec.tsx +30 -3
  32. package/src/scripts/users/__tests__/user-query.spec.tsx +6 -0
  33. package/src/scripts/users/__tests__/user-routes.spec.tsx +42 -18
  34. package/src/scripts/users/components/user-dataset-filters.tsx +157 -0
  35. package/src/scripts/users/dataset-card.tsx +121 -0
  36. package/src/scripts/users/fragments/query.js +42 -0
  37. package/src/scripts/users/scss/datasetcard.module.scss +153 -0
  38. package/src/scripts/users/scss/usernotifications.module.scss +159 -0
  39. package/src/scripts/users/user-account-view.tsx +1 -12
  40. package/src/scripts/users/user-card.tsx +1 -14
  41. package/src/scripts/users/user-container.tsx +1 -17
  42. package/src/scripts/users/user-datasets-view.tsx +58 -43
  43. package/src/scripts/users/user-notification-accordion.tsx +160 -0
  44. package/src/scripts/users/user-notification-list.tsx +27 -0
  45. package/src/scripts/users/user-notifications-tab-content.tsx +85 -0
  46. package/src/scripts/users/user-notifications-view.tsx +102 -4
  47. package/src/scripts/users/user-query.tsx +6 -14
  48. package/src/scripts/users/user-routes.tsx +18 -19
  49. package/src/scripts/utils/__tests__/user-datasets.spec.tsx +86 -0
  50. package/src/scripts/utils/gtag.js +3 -2
  51. package/src/scripts/utils/user-datasets.tsx +60 -0
  52. package/src/scripts/validation/__tests__/__snapshots__/validation-issues.spec.tsx.snap +1 -122
  53. package/src/scripts/validation/validation-results-query.ts +44 -0
  54. package/src/scripts/validation/validation-results.tsx +31 -7
  55. package/src/scripts/validation/validation.tsx +58 -49
  56. package/src/scripts/workers/schema.worker.ts +2 -7
  57. package/tsconfig.json +1 -2
  58. package/vite.config.js +1 -0
@@ -0,0 +1,42 @@
1
+ import { gql } from "@apollo/client"
2
+
3
+ export const INDEX_DATASET_FRAGMENT = gql`
4
+ fragment DatasetIndex on Dataset {
5
+ id
6
+ created
7
+ name
8
+ public
9
+ analytics {
10
+ views
11
+ downloads
12
+ }
13
+ stars {
14
+ userId
15
+ datasetId
16
+ }
17
+ followers {
18
+ userId
19
+ datasetId
20
+ }
21
+ latestSnapshot {
22
+ id
23
+ size
24
+ created
25
+ issues {
26
+ severity
27
+ }
28
+ description {
29
+ Name
30
+ Authors
31
+ SeniorAuthor
32
+ DatasetType
33
+ }
34
+ }
35
+ draft {
36
+ id
37
+ }
38
+ uploader {
39
+ id
40
+ }
41
+ }
42
+ `
@@ -0,0 +1,153 @@
1
+ .userDsWrap {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ justify-content: space-between;
5
+ }
6
+
7
+ .userDsCard {
8
+ background: #fff;
9
+ border: 1px solid #ddd;
10
+ width: calc(50% - 10px);
11
+ padding: 10px;
12
+ margin-bottom: 20px;
13
+ display: flex;
14
+ justify-content: space-between;
15
+ flex-direction: column;
16
+
17
+ h4 {
18
+ margin: 0 0 30px;
19
+ font-size: 19px;
20
+ }
21
+
22
+ .userDsFooter {
23
+ display: flex;
24
+ justify-content: space-between;
25
+
26
+ .userMetawrap {
27
+ width: 90%;
28
+ display: flex;
29
+ flex-direction: column;
30
+
31
+ span {
32
+ color: #777;
33
+
34
+ b {
35
+ color: #000;
36
+ }
37
+
38
+ &:first-child {
39
+ font-size: 13px;
40
+ margin-bottom: 10px;
41
+ }
42
+
43
+ &:last-child {
44
+ text-transform: uppercase;
45
+ font-weight: bold;
46
+
47
+ b {
48
+ text-transform: none;
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ .userIconwrap {
55
+ display: flex;
56
+ justify-content: flex-end;
57
+ align-items: center;
58
+ }
59
+ }
60
+ }
61
+
62
+ .userDSfilters {
63
+ display: flex;
64
+ justify-content: flex-end;
65
+ flex-wrap: wrap;
66
+ margin-bottom: 20px;
67
+ padding-bottom:20px;
68
+ align-items: center;
69
+ z-index: 10000;
70
+ position: relative;
71
+ border-bottom: 2px solid #eee;
72
+
73
+ input {
74
+ min-width: 50%;
75
+ margin-right: auto;
76
+ }
77
+
78
+ .filterDiv,
79
+ .sortDiv {
80
+ cursor: pointer;
81
+ display: flex;
82
+ align-items: center;
83
+ position: relative;
84
+ margin-bottom: 5px;
85
+ }
86
+
87
+ /* Caret icon */
88
+ .filterDiv span::after,
89
+ .sortDiv span::after {
90
+ content: "";
91
+ width: 0;
92
+ height: 0;
93
+ border-left: 5px solid transparent;
94
+ border-right: 5px solid transparent;
95
+ border-top: 5px solid #333;
96
+ margin-left: 10px;
97
+ transition: transform 0.3s ease;
98
+ display: inline-block;
99
+ margin-bottom: 3px;
100
+ }
101
+
102
+ .filterDiv.open span::after,
103
+ .sortDiv.open span::after {
104
+ transform: rotate(180deg); /* Rotate the caret when open */
105
+ }
106
+
107
+ .filterDropdown,
108
+ .sortDropdown {
109
+ position: relative;
110
+
111
+ ul {
112
+ position: absolute;
113
+ list-style-type: none;
114
+ display: flex;
115
+ flex-direction: column;
116
+ margin: 0;
117
+ padding: 20px;
118
+ background-color: white;
119
+ border: 1px solid #ddd;
120
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
121
+ right: 0;
122
+ top: 20px;
123
+ min-width: 200px;
124
+ li {
125
+ padding: 8px;
126
+ cursor: pointer;
127
+
128
+ &:hover {
129
+ background-color: #f4f4f4;
130
+ }
131
+
132
+ &.active {
133
+ font-weight: bold;
134
+ color: var(--on-dark-aqua);
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ .filterDropdown {
141
+ ul {min-width: 120px;}
142
+ }
143
+ .sortDiv {
144
+ margin-left: 20px;
145
+ }
146
+ }
147
+
148
+
149
+ .resultsSummary{
150
+ width: 100%;
151
+ font-size: 13px;
152
+ margin: 10px 0 0;
153
+ }
@@ -0,0 +1,159 @@
1
+ .tabContainer {
2
+ position: relative;
3
+ border-bottom: 2px solid #eee;
4
+ margin-bottom: 40px;
5
+ .tabs {
6
+ display: flex;
7
+ justify-content: flex-start;
8
+ align-items: flex-start;
9
+ list-style: none;
10
+ margin: 0;
11
+ padding: 0;
12
+ position: relative;
13
+
14
+ li {
15
+ position: relative;
16
+ margin: 0 20px 0 0;
17
+
18
+ a {
19
+ &.tabUnread,
20
+ &.tabSaved,
21
+ &.tabArchived {
22
+ display: flex;
23
+ text-decoration: none;
24
+ padding: 10px;
25
+ display: flex;
26
+ align-items: center;
27
+ position: relative;
28
+ z-index: 1;
29
+ .tabicon {
30
+ width: 20px;
31
+ margin-right: 5px;
32
+ }
33
+ &.tabUnread .tabicon {
34
+ width: 18px;
35
+ }
36
+ &.tabArchived .tabicon {
37
+ width: 16px;
38
+ }
39
+ span {
40
+ background-color: #eee;
41
+ padding: 2px 5px;
42
+ margin: 0 0 0 10px;
43
+ display: inline-block;
44
+ border-radius: 4px;
45
+ color: #000;
46
+ font-weight: normal;
47
+ transition: background-color 0.3s, color 0.3s;
48
+ }
49
+ &.active {
50
+ span {
51
+ color: #fff;
52
+ background-color: #b20000;
53
+ }
54
+ }
55
+
56
+ &:hover {
57
+ color: #555;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+ .notificationsList {
65
+ list-style: none;
66
+ margin: 0;
67
+ padding: 0;
68
+ .notificationAccordion {
69
+ border: 1px solid #eee;
70
+ margin-bottom: 20px;
71
+ border-radius: 4px;
72
+ .header {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ padding: 10px;
76
+
77
+ .accordiontitle{
78
+ flex-grow: 1;
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: center;
82
+ }
83
+ h3 {
84
+ font-weight: normal;
85
+ margin: 0;
86
+ }
87
+ button {
88
+ margin: 0;
89
+ border: 0;
90
+ padding: 0;
91
+ background: none;
92
+ cursor: pointer;
93
+ h3 {
94
+ text-decoration: underline;
95
+ color: var(--on-dark-aqua);
96
+ }
97
+ &:disabled {
98
+ pointer-events: none;
99
+ opacity: 0.5;
100
+ cursor: not-allowed;
101
+ }
102
+ }
103
+ .readbutton{
104
+ color: var(--on-dark-aqua);
105
+ background-color:#f3fdff;
106
+ border-radius: 4px;
107
+ border: 1px solid #ccc;
108
+ padding: 5px;
109
+ font-size: 13px;
110
+ text-transform: uppercase;
111
+ margin-right: 5px;
112
+ > i {
113
+ margin-right: 5px;
114
+ }
115
+ }
116
+ .actions {
117
+ display: flex;
118
+ align-items: stretch;
119
+ .notificationdeny, .notificationapprove {
120
+ color: #127B22;
121
+ background-color: #F5FFF5;
122
+ border-radius: 4px;
123
+ border: 1px solid #ccc;
124
+ padding: 5px;
125
+ font-size: 13px;
126
+ text-transform: uppercase;
127
+ margin-right: 5px;
128
+ > i {
129
+ margin-right: 5px;
130
+ }
131
+ }
132
+ .notificationdeny {
133
+ color: #770D0D;
134
+ background-color: #FFF5F5;
135
+ }
136
+
137
+ .accordionicon {
138
+ max-width: 21px;
139
+ height: auto;
140
+ display: inline-block;
141
+ border-radius: 4px;
142
+ border: 1px solid #ccc;
143
+ padding: 5px;
144
+ margin-right: 5px;
145
+ &.archiveicon {
146
+ max-width: 18px;
147
+ }
148
+ &.saveicon {
149
+ max-width: 23px;
150
+ }
151
+ }
152
+ }
153
+ }
154
+ .accordionbody{
155
+ padding: 10px;
156
+ border-top: 1px solid #eee;
157
+ }
158
+ }
159
+ }
@@ -3,18 +3,7 @@ import { useMutation } from "@apollo/client"
3
3
  import { EditableContent } from "./components/editable-content"
4
4
  import styles from "./scss/useraccountview.module.scss"
5
5
  import { GET_USER_BY_ORCID, UPDATE_USER } from "./user-query"
6
-
7
- interface UserAccountViewProps {
8
- user: {
9
- name: string
10
- email: string
11
- orcid: string
12
- links: string[]
13
- location: string
14
- institution: string
15
- github?: string
16
- }
17
- }
6
+ import type { UserAccountViewProps } from "../types/user-types"
18
7
 
19
8
  export const UserAccountView: React.FC<UserAccountViewProps> = ({ user }) => {
20
9
  const [userLinks, setLinks] = useState<string[]>(user.links || [])
@@ -1,19 +1,6 @@
1
1
  import React from "react"
2
2
  import styles from "./scss/usercard.module.scss"
3
-
4
- export interface User {
5
- name: string
6
- location?: string
7
- email: string
8
- orcid: string
9
- institution?: string
10
- links?: string[]
11
- github?: string
12
- }
13
-
14
- export interface UserCardProps {
15
- user: User
16
- }
3
+ import type { UserCardProps } from "../types/user-types"
17
4
 
18
5
  export const UserCard: React.FC<UserCardProps> = ({ user }) => {
19
6
  const { location, institution, email, orcid, links = [], github, name } = user
@@ -3,23 +3,7 @@ import { Outlet } from "react-router-dom"
3
3
  import { UserCard } from "./user-card"
4
4
  import { UserAccountTabs } from "./user-tabs"
5
5
  import styles from "./scss/usercontainer.module.scss"
6
-
7
- interface User {
8
- id: string
9
- name: string
10
- location: string
11
- github?: string
12
- institution: string
13
- email: string
14
- avatar: string
15
- orcid: string
16
- links: string[]
17
- }
18
-
19
- interface AccountContainerProps {
20
- user: User
21
- hasEdit: boolean
22
- }
6
+ import type { AccountContainerProps } from "../types/user-types"
23
7
 
24
8
  export const UserAccountContainer: React.FC<AccountContainerProps> = ({
25
9
  user,
@@ -1,53 +1,68 @@
1
- import React from "react"
1
+ import React, { useState } from "react"
2
+ import { DatasetCard } from "./dataset-card"
3
+ import { UserDatasetFilters } from "./components/user-dataset-filters"
4
+ import { gql, useQuery } from "@apollo/client"
5
+ import styles from "./scss/datasetcard.module.scss"
6
+ import type { Dataset, UserDatasetsViewProps } from "../types/user-types"
7
+ import { INDEX_DATASET_FRAGMENT } from "./fragments/query"
8
+ import { filterAndSortDatasets } from "../utils/user-datasets"
2
9
 
3
- interface User {
4
- name: string
5
- }
10
+ export const DATASETS_QUERY = gql`
11
+ query Datasets($first: Int) {
12
+ datasets(first: $first) {
13
+ edges {
14
+ node {
15
+ ...DatasetIndex
16
+ }
17
+ }
18
+ }
19
+ }
20
+ ${INDEX_DATASET_FRAGMENT}
21
+ `
6
22
 
7
- interface Dataset {
8
- id: string
9
- created: string
10
- ownerId: string
11
- name: string
12
- type: string
13
- }
23
+ export const UserDatasetsView: React.FC<UserDatasetsViewProps> = (
24
+ { user, hasEdit },
25
+ ) => {
26
+ const [searchQuery, setSearchQuery] = useState("")
27
+ const [publicFilter, setPublicFilter] = useState<string>("all")
28
+ const [sortOrder, setSortOrder] = useState<string>("date-updated")
14
29
 
15
- interface UserDatasetsViewProps {
16
- user: User
17
- }
30
+ const { data, loading, error } = useQuery(DATASETS_QUERY, {
31
+ variables: { first: 25 },
32
+ })
33
+
34
+ if (loading) return <p>Loading datasets...</p>
35
+ if (error) return <p>Failed to fetch datasets: {error.message}</p>
36
+
37
+ const datasets: Dataset[] =
38
+ data?.datasets?.edges?.map((edge: { node: Dataset }) => edge.node) || []
39
+
40
+ const filteredAndSortedDatasets = filterAndSortDatasets(datasets, {
41
+ searchQuery,
42
+ publicFilter,
43
+ sortOrder,
44
+ })
18
45
 
19
- const dummyDatasets: Dataset[] = [
20
- {
21
- id: "ds00001",
22
- created: "2023-11-01T12:00:00Z",
23
- ownerId: "1",
24
- name: "Dataset 1",
25
- type: "public",
26
- },
27
- {
28
- id: "ds00002",
29
- created: "2023-11-02T12:00:00Z",
30
- ownerId: "2",
31
- name: "Dataset 2",
32
- type: "private",
33
- },
34
- ]
35
-
36
- export const UserDatasetsView: React.FC<UserDatasetsViewProps> = ({ user }) => {
37
46
  return (
38
47
  <div data-testid="user-datasets-view">
39
- <h1>{user.name}'s Datasets</h1>
40
- <div>
41
- {dummyDatasets.map((dataset) => (
42
- <div key={dataset.id} data-testid={`dataset-${dataset.id}`}>
43
- <h2>{dataset.name}</h2>
44
- <p>Type: {dataset.type}</p>
45
- <p>Created: {dataset.created}</p>
46
- </div>
47
- ))}
48
+ <h3>{user.name}'s Datasets</h3>
49
+
50
+ <UserDatasetFilters
51
+ publicFilter={publicFilter}
52
+ setPublicFilter={setPublicFilter}
53
+ sortOrder={sortOrder}
54
+ setSortOrder={setSortOrder}
55
+ searchQuery={searchQuery}
56
+ setSearchQuery={setSearchQuery}
57
+ />
58
+
59
+ <div className={styles.userDsWrap}>
60
+ {filteredAndSortedDatasets.length > 0
61
+ ? filteredAndSortedDatasets.map((dataset) => (
62
+ <DatasetCard key={dataset.id} dataset={dataset} hasEdit={hasEdit} />
63
+ ))
64
+ : <p>No datasets found.</p>}
48
65
  </div>
49
66
  </div>
50
67
  )
51
68
  }
52
-
53
- export default UserDatasetsView
@@ -0,0 +1,160 @@
1
+ import React, { useState } from "react"
2
+ import styles from "./scss/usernotifications.module.scss"
3
+ import { Tooltip } from "@openneuro/components/tooltip"
4
+ import iconUnread from "../../assets/icon-unread.png"
5
+ import iconSaved from "../../assets/icon-saved.png"
6
+ import iconArchived from "../../assets/icon-archived.png"
7
+
8
+ export const NotificationAccordion = ({ notification, onUpdate }) => {
9
+ const { id, title, content, status, type, approval } = notification
10
+
11
+ const hasContent = content && content.trim().length > 0
12
+
13
+ const [isOpen, setIsOpen] = useState(false)
14
+ const toggleAccordion = () => setIsOpen(!isOpen)
15
+
16
+ const handleApprovalChange = (approvalStatus) => {
17
+ onUpdate(id, { approval: approvalStatus })
18
+ }
19
+
20
+ const handleStatusChange = (newStatus) => {
21
+ onUpdate(id, { status: newStatus })
22
+ }
23
+
24
+ return (
25
+ <li
26
+ className={`${styles.notificationAccordion} ${isOpen ? styles.open : ""}`}
27
+ >
28
+ <div className={styles.header}>
29
+ {/* Render title as button if content exists, otherwise as plain text */}
30
+ <h3 className={styles.accordiontitle}>{title}</h3>
31
+
32
+ {hasContent && (
33
+ <button className={styles.readbutton} onClick={toggleAccordion}>
34
+ {isOpen
35
+ ? (
36
+ <span>
37
+ <i className="fa fa-times"></i> Close
38
+ </span>
39
+ )
40
+ : (
41
+ <span>
42
+ <i className="fa fa-eye"></i> Review
43
+ </span>
44
+ )}
45
+ </button>
46
+ )}
47
+ <div className={styles.actions}>
48
+ {type === "approval" && (
49
+ <>
50
+ {(approval === "not provided" || approval === "approved") && (
51
+ <button
52
+ className={`${styles.notificationapprove} ${
53
+ approval === "approved" ? styles.active : ""
54
+ }`}
55
+ onClick={() => handleApprovalChange("approved")}
56
+ disabled={approval === "approved"}
57
+ >
58
+ <i className="fa fa-check"></i>{" "}
59
+ {approval === "approved" ? "Approved" : "Approve"}
60
+ </button>
61
+ )}
62
+
63
+ {(approval === "not provided" || approval === "denied") && (
64
+ <button
65
+ className={`${styles.notificationdeny} ${
66
+ approval === "denied" ? styles.active : ""
67
+ }`}
68
+ onClick={() => handleApprovalChange("denied")}
69
+ disabled={approval === "denied"}
70
+ >
71
+ <i className="fa fa-times"></i>{" "}
72
+ {approval === "denied" ? "Denied" : "Deny"}
73
+ </button>
74
+ )}
75
+ </>
76
+ )}
77
+ {/* Render actions based on the notification's status */}
78
+ {status === "unread" && (
79
+ <>
80
+ <Tooltip tooltip="Save and mark as read">
81
+ <button
82
+ className={styles.save}
83
+ onClick={() => handleStatusChange("saved")}
84
+ >
85
+ <img
86
+ className={`${styles.accordionicon} ${styles.saveicon}`}
87
+ src={iconSaved}
88
+ alt=""
89
+ />
90
+ <span className="sr-only">Save</span>
91
+ </button>
92
+ </Tooltip>
93
+ <Tooltip tooltip="Archive">
94
+ <button
95
+ className={styles.archive}
96
+ onClick={() => handleStatusChange("archived")}
97
+ >
98
+ <img
99
+ className={`${styles.accordionicon} ${styles.archiveicon}`}
100
+ src={iconArchived}
101
+ alt=""
102
+ />
103
+ <span className="sr-only">Archive</span>
104
+ </button>
105
+ </Tooltip>
106
+ </>
107
+ )}
108
+ {status === "saved" && (
109
+ <>
110
+ <Tooltip tooltip="Mark as Unread">
111
+ <button
112
+ className={styles.unread}
113
+ onClick={() => handleStatusChange("unread")}
114
+ >
115
+ <img
116
+ className={`${styles.accordionicon} ${styles.unreadicon}`}
117
+ src={iconUnread}
118
+ alt=""
119
+ />
120
+ <span className="sr-only">Mark as Unread</span>
121
+ </button>
122
+ </Tooltip>
123
+ <Tooltip tooltip="Archive">
124
+ <button
125
+ className={styles.archive}
126
+ onClick={() => handleStatusChange("archived")}
127
+ >
128
+ <img
129
+ className={`${styles.accordionicon} ${styles.archiveicon}`}
130
+ src={iconArchived}
131
+ alt=""
132
+ />
133
+ <span className="sr-only">Archive</span>
134
+ </button>
135
+ </Tooltip>
136
+ </>
137
+ )}
138
+ {status === "archived" && (
139
+ <Tooltip tooltip="Mark as Unread">
140
+ <button
141
+ className={styles.unarchive}
142
+ onClick={() => handleStatusChange("unread")}
143
+ >
144
+ <img
145
+ className={`${styles.accordionicon} ${styles.unreadicon}`}
146
+ src={iconUnread}
147
+ alt=""
148
+ />
149
+ <span className="sr-only">Unarchive</span>
150
+ </button>
151
+ </Tooltip>
152
+ )}
153
+ </div>
154
+ </div>
155
+ {isOpen && hasContent && (
156
+ <div className={styles.accordionbody}>{content}</div>
157
+ )}
158
+ </li>
159
+ )
160
+ }