diff --git a/src/devhub/entity/addon/github/kanban_board.jsx b/src/devhub/entity/addon/github/kanban_board.jsx index e13f27f90..ebb4298d7 100644 --- a/src/devhub/entity/addon/github/kanban_board.jsx +++ b/src/devhub/entity/addon/github/kanban_board.jsx @@ -72,23 +72,25 @@ const GithubKanbanBoard = ({ : []; const issues = dataTypesIncluded.Issue - ? DataRequest?.paginated( - (pageNumber) => - useCache( - () => - asyncFetch( - `https://api.github.com/repos/${repoURL - .split("/") - .slice(-2) - .concat(["issues"]) - .join( - "/" - )}?state=${ticketStateFilter}&per_page=100&page=${pageNumber}` - ).then((res) => res?.body), - repoURL + pageNumber, - { subscribe: false } - ), - { startWith: 1 } + ? ( + DataRequest?.paginated( + (pageNumber) => + useCache( + () => + asyncFetch( + `https://api.github.com/repos/${repoURL + .split("/") + .slice(-2) + .concat(["issues"]) + .join( + "/" + )}?state=${ticketStateFilter}&per_page=100&page=${pageNumber}` + ).then((res) => res?.body), + repoURL + pageNumber, + { subscribe: false } + ), + { startWith: 1 } + ) ?? [] )?.map(withType("Issue")) : []; @@ -103,7 +105,7 @@ const GithubKanbanBoard = ({ return (
-
+
{title}
@@ -127,12 +129,17 @@ const GithubKanbanBoard = ({ const tickets = state.ticketsByColumn[column.id] ?? []; return ( -
+
diff --git a/src/devhub/entity/addon/github/kanban_ticket.jsx b/src/devhub/entity/addon/github/kanban_ticket.jsx index 67af832f4..6a97093b4 100644 --- a/src/devhub/entity/addon/github/kanban_ticket.jsx +++ b/src/devhub/entity/addon/github/kanban_ticket.jsx @@ -93,7 +93,7 @@ const GithubKanbanTicket = ({ const labelList = features.labels ? (
- {labels.map((label) => ( + {(labels ?? []).map((label) => ( + {header} -
+
{titleArea} {labelList}
diff --git a/src/devhub/entity/addon/kanban/Configurator.jsx b/src/devhub/entity/addon/kanban/Configurator.jsx index 1af5109e1..ffe1480d7 100644 --- a/src/devhub/entity/addon/kanban/Configurator.jsx +++ b/src/devhub/entity/addon/kanban/Configurator.jsx @@ -28,42 +28,19 @@ const settings = { }; const KanbanPostBoardBasicInfoSchema = { - title: { label: "Title", order: 1, placeholder: "Enter board title." }, - + title: { label: "Title", order: 1, placeholder: "Enter board title" }, description: { label: "Description", order: 2, - placeholder: "Enter board description.", - }, -}; - -const KanbanPostBoardTagsSchema = { - required: { - label: - "Enter tags you want to include. Posts with these tags will display.", - - order: 1, - placeholder: "tag1, tag2", - }, - - excluded: { - label: - "Enter tags you want to exclude. Posts with these tags will not show.", - - order: 2, - placeholder: "tag3, tag4", + placeholder: "Enter board description", }, }; const KanbanPostBoardTicketFeaturesSchema = { author: { label: "Author" }, like_count: { label: "Likes" }, - reply_count: { label: "Replies", noop: true }, - sponsorship_request_indicator: { label: "Sponsorship request indicator" }, - requested_sponsorship_value: { label: "Amount of requested funds" }, - requested_sponsor: { label: "Requested sponsor" }, - approved_sponsorship_value: { label: "Approved amount" }, - sponsorship_supervisor: { label: "Supervisor" }, + approved_sponsorship_value: { label: "Funding amount" }, + sponsorship_supervisor: { label: "Supervisor/Sponser" }, tags: { label: "Tags" }, type: { label: "Post type" }, }; @@ -79,20 +56,16 @@ const KanbanPostBoardDefaults = { features: { author: true, like_count: true, - reply_count: false, - sponsorship_request_indicator: false, - requested_sponsorship_value: false, - requested_sponsor: false, approved_sponsorship_value: true, sponsorship_supervisor: true, tags: true, type: true, }, + sortBy: "", }, }, payload: { columns: {}, - tags: { excluded: [], required: [] }, }, }; @@ -116,6 +89,20 @@ const toMigrated = ({ config, metadata, payload }) => ({ }, }); +const sortByOptions = [ + { label: "None", value: "none" }, + { label: "Amount: High to Low", value: "descending-amount" }, + { label: "Amount: Low to High", value: "ascending-amount" }, + { label: "Date: Newest to Oldest", value: "descending-date" }, + { label: "Date: Oldest to Newest", value: "ascending-date" }, + { label: "Author: A-Z", value: "ascending-author" }, + { label: "Author: Z-A", value: "descending-author" }, + { label: "Sponsor/Supervisor: A-Z", value: "ascending-sponsor" }, + { label: "Sponsor/Supervisor: Z-A", value: "descending-sponsor" }, + { label: "Most Likes", value: "descending-likes" }, + { label: "Fewest Likes", value: "ascending-likes" }, +]; + const KanbanViewConfigurator = ({ handle, data, permissions, onSubmit }) => { const tags = useCache( () => @@ -200,9 +187,9 @@ const KanbanViewConfigurator = ({ handle, data, permissions, onSubmit }) => { const onSave = () => onSubmit(formState); const formElement = ( - <> -
-
+
+
+
{ }} />
- -
- -
- - - {`Columns ( max. ${settings.maxColumnsNumber} )`} - -
+
+ + Fields to display + +
+ +
+
+
+ + Sort by + +
+
+ +
+
+
-
- {Object.values(formState.payload.columns ?? {}).map( - ({ id, description, tag, title }) => ( - -
- - - -
+
+ + + {`Columns ( max. ${settings.maxColumnsNumber} )`} + +
-
+ {Object.values(formState.payload.columns ?? {}).map( + ({ id, description, tag, title }) => ( + - -
-
- ) - )} +
+ + +
+ + Enter a tag to filter posts in this column + +
+ +
+
+
-
- - +
+ +
+ + ) + )} +
+
+
+ = + settings.maxColumnsNumber, + icon: { type: "bootstrap_icon", variant: "bi-plus-lg" }, + onClick: formUpdate({ + path: ["payload", "columns"], + via: columnsCreateNew, + }), + }} + /> +
+
+ + +
- +
); return ( @@ -395,28 +447,7 @@ const KanbanViewConfigurator = ({ handle, data, permissions, onSubmit }) => { Kanban board configuration
- {Object.keys(formState.metadata ?? {}).length > 0 && ( -
- {formElement} - = - settings.maxColumnsNumber, - icon: { type: "bootstrap_icon", variant: "bi-plus-lg" }, - onClick: formUpdate({ - path: ["payload", "columns"], - via: columnsCreateNew, - }), - }} - /> -
- )} + {Object.keys(formState.metadata ?? {}).length > 0 && formElement}
)} {!Object.keys(formState.metadata ?? {}).length && ( diff --git a/src/devhub/entity/addon/kanban/post_board.jsx b/src/devhub/entity/addon/kanban/post_board.jsx index e14116002..36ba05e45 100644 --- a/src/devhub/entity/addon/kanban/post_board.jsx +++ b/src/devhub/entity/addon/kanban/post_board.jsx @@ -1,6 +1,10 @@ const { getPostsByLabel } = VM.require( "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" ); +const { getPost } = VM.require( + "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" +); +getPost || (getPost = () => {}); getPostsByLabel || (getPostsByLabel = () => {}); const postTagsToIdSet = (tags) => { @@ -9,6 +13,19 @@ const postTagsToIdSet = (tags) => { ); }; +const sortByValues = { + descendingAmount: "descending-amount", + ascendingAmount: "ascending-amount", + descendingDate: "descending-date", + ascendingDate: "ascending-date", + ascendingAuthor: "ascending-author", + descendingAuthor: "descending-author", + ascendingSponsor: "ascending-sponsor", + descendingSponsor: "descending-sponsor", + descendingLikes: "descending-likes", + ascendingLikes: "ascending-likes", +}; + const configToColumnData = ({ columns, tags }) => Object.entries(columns).reduce((registry, [columnId, column]) => { const postIds = (getPostsByLabel({ label: column.tag }) ?? []).reverse(); @@ -16,28 +33,101 @@ const configToColumnData = ({ columns, tags }) => ...registry, [columnId]: { ...column, - postIds: - tags.required.length > 0 - ? postIds.filter( - (postId) => - postTagsToIdSet(tags.required).has(postId) && - !postTagsToIdSet(tags.excluded).has(postId) - ) - : postIds, + postIds: postIds, }, }; }, {}); +const basicAlphabeticalComparison = (a, b) => { + if (a < b) return -1; + if (a > b) return 1; + return 0; +}; + const KanbanPostBoard = ({ metadata, payload }) => { - const columns = Object.entries(configToColumnData(payload) ?? {}).map( - ([columnId, column]) => ( -
+ const boardData = Object.entries(configToColumnData(payload) ?? {}); + + const view = boardData.map(([columnId, column]) => { + const data = []; + column.postIds?.map((postId) => { + if (postId) { + const postData = getPost({ + post_id: postId ? parseInt(postId) : 0, + }); + data.push(postData); + } + }); + + // sort data by selected sorting mechanism + switch (metadata.ticket.sortBy) { + case sortByValues.descendingAmount: + data.sort((a, b) => b?.snapshot?.amount - a?.snapshot?.amount); + break; + case sortByValues.ascendingAmount: + data.sort((a, b) => a?.snapshot?.amount - b?.snapshot?.amount); + break; + case sortByValues.descendingDate: + data.sort( + (a, b) => + parseInt(b?.snapshot?.timestamp) - parseInt(a?.snapshot?.timestamp) + ); + break; + case sortByValues.ascendingDate: + data.sort( + (a, b) => + parseInt(a?.snapshot?.timestamp) - parseInt(b?.snapshot?.timestamp) + ); + break; + case sortByValues.ascendingAuthor: + data.sort((a, b) => + basicAlphabeticalComparison(a.author_id, b.author_id) + ); + break; + case sortByValues.descendingAuthor: + data.sort((a, b) => + basicAlphabeticalComparison(b.author_id, a.author_id) + ); + break; + case sortByValues.ascendingSponsor: + data.sort((a, b) => + basicAlphabeticalComparison( + a?.snapshot?.requested_sponsor || a?.snapshot?.supervisor, + b?.snapshot?.requested_sponsor || b?.snapshot?.supervisor + ) + ); + break; + case sortByValues.descendingSponsor: + data.sort((a, b) => + basicAlphabeticalComparison( + b?.snapshot?.requested_sponsor || b?.snapshot?.supervisor, + a?.snapshot?.requested_sponsor || a?.snapshot?.supervisor + ) + ); + break; + case sortByValues.descendingLikes: + data.sort((a, b) => b.likes.length - a.likes.length); + break; + case sortByValues.ascendingLikes: + data.sort((a, b) => a.likes.length - b.likes.length); + break; + default: + data; + break; + } + + return ( +
@@ -51,33 +141,41 @@ const KanbanPostBoard = ({ metadata, payload }) => {

{column.description}

-
- {column.postIds?.map((postId) => ( - - ))} +
+ {data.length === column.postIds.length && + data.map((postData) => ( + + ))}
- ) - ); + ); + }); return (
-
-
+
+
{metadata?.title}
-

+

{metadata?.description}

-
+
{ > No columns were created so far.
- - {columns} + {view}
); diff --git a/src/devhub/entity/addon/kanban/post_ticket.jsx b/src/devhub/entity/addon/kanban/post_ticket.jsx index 2164fbdcd..56f7a4d67 100644 --- a/src/devhub/entity/addon/kanban/post_ticket.jsx +++ b/src/devhub/entity/addon/kanban/post_ticket.jsx @@ -1,8 +1,6 @@ +const columnId = props.columnId; + const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url"); -const { getPost } = VM.require( - "${REPL_DEVHUB}/widget/core.adapter.devhub-contract" -); -getPost || (getPost = () => {}); href || (href = () => {}); const AttractableDiv = styled.div` @@ -35,11 +33,7 @@ function getToken(token) { } return amountUnit; } -const KanbanPostTicket = ({ metadata }) => { - const data = getPost({ - post_id: metadata.id ? parseInt(metadata.id) : 0, - }); - +const KanbanPostTicket = ({ metadata, data }) => { if (!data) return
Loading ...
; const { @@ -55,117 +49,76 @@ const KanbanPostTicket = ({ metadata }) => { supervisor, } = data.snapshot; - const isFundingRequested = - post_type === "Solution" && - typeof requested_sponsorship_amount === "string" && - parseInt(requested_sponsorship_amount, 10) > 0; - const features = { ...metadata.features, - - sponsorship_request_indicator: - isFundingRequested && metadata.features.sponsorship_request_indicator, - - requested_sponsorship_value: - isFundingRequested && metadata.features.requested_sponsorship_value, - approved_sponsorship_value: post_type === "Sponsorship" && metadata.features.approved_sponsorship_value, - - requested_sponsor: - isFundingRequested && metadata.features.requested_sponsor, - sponsorship_supervisor: post_type === "Sponsorship" && metadata.features.sponsorship_supervisor, }; - const header = ( -
- - {features.author ? ( + const footer = ( + ); - const footer = - features.like_count || features.reply_count ? ( -
- {features.like_count ? ( - - - - {data.likes.length} - - ) : null} - - {features.reply_count ? ( - - - - {data.comments.length} - - ) : null} -
- ) : null; - const titleArea = ( - + {features.type ? ( ) : null} + {name} + + ); - - {[features.type ? post_type : null, name] - .filter( - (maybeString) => - typeof maybeString === "string" && maybeString.length > 0 - ) - .join(": ")} - + const sponsorshipValue = ( + + {requested_sponsorship_amount ?? amount} + {requested_sponsorship_token ?? getToken(sponsorship_token)} ); + const requestedSponsor = ( + + ); + const descriptionArea = post_type === "Comment" ? ( -
+
) : null; @@ -181,65 +134,52 @@ const KanbanPostTicket = ({ metadata }) => {
) : null; - return ( - - {header} + const showFunding = features.approved_sponsorship_value; + const showSponsor = features.sponsorship_supervisor; -
- {titleArea} + return ( + +
+
+ {titleArea} + {features.author && ( + + + + )} +
{descriptionArea} - - {features.sponsorship_request_indicator ? ( - - - - Funding requested - - ) : null} - - {features.requested_sponsorship_value || - features.approved_sponsorship_value ? ( + {showFunding && ( - - {post_type === "Solution" ? "Requested" : "Approved"} funding: - - - - {requested_sponsorship_amount ?? amount} - - {requested_sponsorship_token ?? getToken(sponsorship_token)} - - + Amount: + {sponsorshipValue} - ) : null} - - {features.requested_sponsor || features.sponsorship_supervisor ? ( + )} + {showSponsor && (
{`${ - post_type === "Solution" ? "Requested sponsor" : "Supervisor" - }:`} - - + post_type === "Solution" ? "Sponsor" : "Supervisor" + }:`}{" "} + {requestedSponsor}{" "}
- ) : null} - + )} {tagList}
- {footer}
); diff --git a/src/devhub/entity/post/PostEditor.jsx b/src/devhub/entity/post/PostEditor.jsx index d7570c137..8fb798e93 100644 --- a/src/devhub/entity/post/PostEditor.jsx +++ b/src/devhub/entity/post/PostEditor.jsx @@ -1,5 +1,6 @@ -const { normalize } = - VM.require("${REPL_DEVHUB}/widget/core.lib.stringUtils") || (() => {}); +const { normalize } = VM.require("${REPL_DEVHUB}/widget/core.lib.stringUtils"); + +normalize || (normalize = () => {}); const CenteredMessage = styled.div` display: flex; diff --git a/src/devhub/feature/post-search/by-tag.jsx b/src/devhub/feature/post-search/by-tag.jsx index e86efee04..b79998b71 100644 --- a/src/devhub/feature/post-search/by-tag.jsx +++ b/src/devhub/feature/post-search/by-tag.jsx @@ -14,7 +14,8 @@ if (tags === null) { return
Loading ...
; } -const onChange = (selectedTags) => props.onTagSearch?.(selectedTags[0]?.name); +const onChange = (selectedTags) => + props.onTagSearch?.(selectedTags[0]?.name ?? ""); return ( {}); +const { normalize } = VM.require("${REPL_DEVHUB}/widget/core.lib.stringUtils"); + +normalize || (normalize = () => {}); const Button = styled.button` height: 40px;