diff --git a/elm-git.json b/elm-git.json index be14ca24..d43bd696 100644 --- a/elm-git.json +++ b/elm-git.json @@ -1,7 +1,7 @@ { "git-dependencies": { "direct": { - "https://github.com/unisonweb/ui-core": "58d63fe0de338b9355a37262ccc8ed9fb486b806" + "https://github.com/unisonweb/ui-core": "e1cee63aac897c3c6c76ad5a2017f061eb73d978" }, "indirect": {} } diff --git a/src/UnisonShare/Api.elm b/src/UnisonShare/Api.elm index d300062c..382592f8 100644 --- a/src/UnisonShare/Api.elm +++ b/src/UnisonShare/Api.elm @@ -409,6 +409,18 @@ projectBranchReleaseNotes projectRef branchRef = } +projectBranchHistory : ProjectRef -> BranchRef -> Endpoint +projectBranchHistory projectRef branchRef = + let + ( handle, slug ) = + ProjectRef.toApiStringParts projectRef + in + GET + { path = [ "users", handle, "projects", slug, "history", BranchRef.toApiUrlString branchRef ] + , queryParams = [] + } + + -- PROJECT ROLE ASSIGNMENTS (COLLABORATORS) diff --git a/src/UnisonShare/Link.elm b/src/UnisonShare/Link.elm index b3d9b07d..2d1cbb71 100644 --- a/src/UnisonShare/Link.elm +++ b/src/UnisonShare/Link.elm @@ -250,6 +250,11 @@ projectOverview projectRef_ = toClick (Route.projectOverview projectRef_) +projectHistory : ProjectRef -> Maybe BranchRef -> PageCursorParam -> Click msg +projectHistory projectRef_ branchRef_ cursor = + toClick (Route.projectHistory projectRef_ branchRef_ cursor) + + projectBranches : ProjectRef -> PageCursorParam -> Click msg projectBranches projectRef_ cursor = toClick (Route.projectBranches projectRef_ cursor) diff --git a/src/UnisonShare/Page/ProjectHistoryPage.elm b/src/UnisonShare/Page/ProjectHistoryPage.elm new file mode 100644 index 00000000..054c255b --- /dev/null +++ b/src/UnisonShare/Page/ProjectHistoryPage.elm @@ -0,0 +1,351 @@ +module UnisonShare.Page.ProjectHistoryPage exposing (..) + +import Code.BranchRef exposing (BranchRef) +import Code.FullyQualifiedName as FQN +import Code.Hash as Hash +import Html exposing (Html, div, h3, label, p, span, text) +import Html.Attributes exposing (class) +import Lib.Util as Util +import RemoteData exposing (RemoteData(..), WebData) +import String.Extra exposing (pluralize) +import UI +import UI.AnchoredOverlay as AnchoredOverlay +import UI.Card as Card +import UI.DateTime as DateTime +import UI.Icon as Icon exposing (Icon) +import UI.PageContent as PageContent exposing (PageContent) +import UI.PageLayout as PageLayout exposing (PageLayout) +import UI.PageTitle as PageTitle +import UI.Placeholder as Placeholder +import UI.ProfileSnippet as ProfileSnippet +import UnisonShare.AppContext exposing (AppContext) +import UnisonShare.Link as Link +import UnisonShare.Page.ErrorPage as ErrorPage +import UnisonShare.PageFooter as PageFooter +import UnisonShare.Paginated as Paginated exposing (Paginated(..)) +import UnisonShare.Project as Project exposing (ProjectDetails) +import UnisonShare.Project.BranchHistory as BranchHistory exposing (HistoryEntry) +import UnisonShare.Project.ProjectRef exposing (ProjectRef) +import UnisonShare.Route as Route +import UnisonShare.SwitchBranch as SwitchBranch + + + +-- MODEL + + +type alias PaginatedBranchHistory = + Paginated.Paginated HistoryEntry + + +type alias Model = + { history : WebData PaginatedBranchHistory + , switchBranch : SwitchBranch.Model + } + + +preInit : Model +preInit = + { history = NotAsked + , switchBranch = SwitchBranch.init + } + + +init : AppContext -> ProjectDetails -> Maybe BranchRef -> Paginated.PageCursorParam -> ( Model, Cmd Msg ) +init appContext project branchRef cursor = + let + branchRef_ = + Maybe.withDefault (Project.defaultBrowsingBranch project) branchRef + in + ( { history = Loading + , switchBranch = SwitchBranch.init + } + , fetchProjectBranchHistory appContext project.ref branchRef_ cursor + ) + + + +-- UPDATE + + +type Msg + = FetchProjectBranchHistoryFinished BranchRef (WebData PaginatedBranchHistory) + | SwitchBranchMsg SwitchBranch.Msg + + +update : AppContext -> ProjectDetails -> Maybe BranchRef -> Msg -> Model -> ( Model, Cmd Msg ) +update appContext project _ msg model = + case msg of + FetchProjectBranchHistoryFinished _ history -> + ( { model | history = history }, Cmd.none ) + + SwitchBranchMsg sbMsg -> + let + ( switchBranch, switchBranchCmd, out ) = + SwitchBranch.update appContext project.ref sbMsg model.switchBranch + + navCmd = + case out of + SwitchBranch.SwitchToBranchRequest branchRef -> + Route.navigate + appContext.navKey + (Route.projectHistory + project.ref + (Just branchRef) + Paginated.NoPageCursor + ) + + _ -> + Cmd.none + in + ( { model | switchBranch = switchBranch } + , Cmd.batch + [ Cmd.map SwitchBranchMsg switchBranchCmd + , navCmd + ] + ) + + + +-- EFFECTS + + +fetchProjectBranchHistory : AppContext -> ProjectRef -> BranchRef -> Paginated.PageCursorParam -> Cmd Msg +fetchProjectBranchHistory _ _ branchRef _ = + let + {- + params = + { limit = 64 + , cursor = cursor + } + -} + mkPaginated prev next items = + Paginated.Paginated { prev = prev, next = next, items = items } + + mock = + RemoteData.Success + (mkPaginated + Nothing + (Just (Paginated.PageCursor "asdf-1234")) + [ BranchHistory.Comment + { createdAt = DateTime.unsafeFromISO8601 "2025-11-03T13:35:33-05:00" + , afterCausalHash = Hash.unsafeFromString "commenthash1" + , author = BranchHistory.UnverifiedUser { name = "Simon" } + , subject = Just "Just fixed a bunch of stuff" + , body = "And it was really cool" + } + , BranchHistory.Changeset + { hash = Hash.unsafeFromString "namespacehash3" + , updates = + [ { hash = Hash.unsafeFromString "definitionhash1" + , fqn = FQN.fromString "List.map" + } + ] + , removes = [] + , aliases = + [ { hash = Hash.unsafeFromString "definitionhash1" + , fqn = FQN.fromString "List.map" + } + ] + , renames = [] + } + , BranchHistory.Comment + { createdAt = DateTime.unsafeFromISO8601 "2025-11-03T13:35:33-05:00" + , afterCausalHash = Hash.unsafeFromString "commenthash1" + , author = BranchHistory.UnverifiedUser { name = "Simon" } + , subject = Just "Just fixed a bunch of stuff" + , body = "And it was really cool" + } + ] + ) + in + Util.delayMsg 500 (FetchProjectBranchHistoryFinished branchRef mock) + + + +{- + ShareApi.projectBranchHistory projectRef branchRef + |> HttpApi.toRequest + BranchHistory.decode + (RemoteData.fromResult >> FetchProjectBranchHistoryFinished branchRef) + |> HttpApi.perform appContext.api +-} +-- VIEW + + +viewLoadingPage : PageLayout msg +viewLoadingPage = + let + shape length = + Placeholder.text + |> Placeholder.withLength length + |> Placeholder.subdued + |> Placeholder.tiny + |> Placeholder.view + + content = + PageContent.oneColumn + [ Card.card + [ shape Placeholder.Large + , shape Placeholder.Small + , shape Placeholder.Medium + ] + |> Card.asContained + |> Card.view + ] + |> PageContent.withPageTitle (PageTitle.title "History") + in + PageLayout.centeredNarrowLayout content PageFooter.pageFooter + |> PageLayout.withSubduedBackground + + +viewHistoryEntry_ : String -> Icon msg -> { leftTitle : Html msg, rightTitle : Html msg } -> List (Html msg) -> Html msg +viewHistoryEntry_ className icon { leftTitle, rightTitle } cardContent = + div [ class ("history-entry " ++ className) ] + [ div [ class "history-entry_gutter" ] + [ div [ class "icon-wrapper" ] [ Icon.view icon ] + ] + , div [ class "history-entry_title-and-details" ] + [ div [ class "history-entry_title" ] + [ leftTitle + , rightTitle + ] + , Card.card cardContent + |> Card.withClassName "project-history_entry" + |> Card.asContained + |> Card.view + ] + ] + + +viewHistoryEntry : AppContext -> HistoryEntry -> Html msg +viewHistoryEntry appContext entry = + case entry of + BranchHistory.Comment comment -> + let + createdAt = + DateTime.view + (DateTime.DistanceFrom appContext.now) + appContext.timeZone + comment.createdAt + + author = + case comment.author of + BranchHistory.VerifiedShareUser user -> + ProfileSnippet.view (ProfileSnippet.profileSnippet user) + + BranchHistory.UnverifiedUser { name } -> + span [ class "unverified-user" ] [ text name ] + in + viewHistoryEntry_ + "history-entry_comment" + Icon.tag + { leftTitle = author, rightTitle = createdAt } + [ comment.subject + |> Maybe.map (\s -> h3 [] [ text s ]) + |> Maybe.withDefault UI.nothing + , p [] [ text comment.body ] + ] + + BranchHistory.Changeset changeset -> + let + numChanges = + (changeset.updates ++ changeset.removes ++ changeset.aliases ++ changeset.renames) + |> List.length + |> pluralize "change" "changes" + + viewChanges : String -> Icon msg -> String -> List BranchHistory.Change -> Html msg + viewChanges className icon title changes = + if List.isEmpty changes then + UI.nothing + + else + div [ class className ] + [ div [ class "history-entry_changeset_changes_title" ] + [ Icon.view icon, label [] [ text title ] ] + , div [ class "history-entry_changeset_changes_list" ] + (List.map (.fqn >> FQN.view) changes) + ] + + viewCardContent : BranchHistory.ChangesetDetails -> Html msg + viewCardContent { updates, removes, aliases, renames } = + div [ class "history-entry_changeset_changes" ] + [ viewChanges "updates" Icon.largePlus "Adds/Updates" updates + , viewChanges "removes" Icon.trash "Removes" removes + , viewChanges "aliases" Icon.tags "Aliases" aliases + , viewChanges "renames" Icon.tag "Renames" renames + ] + in + viewHistoryEntry_ + "history-entry_changeset" + Icon.historyNode + { leftTitle = Hash.view changeset.hash, rightTitle = text numChanges } + [ viewCardContent changeset ] + + +viewPaginationControls : ProjectRef -> Maybe BranchRef -> { a | prev : Maybe Paginated.PageCursor, next : Maybe Paginated.PageCursor } -> Html msg +viewPaginationControls projectRef branchRef cursors = + let + toLink cursor = + Link.projectHistory projectRef branchRef cursor + in + Paginated.view toLink cursors + + +viewPageContent : AppContext -> ProjectDetails -> Maybe BranchRef -> SwitchBranch.Model -> PaginatedBranchHistory -> PageContent Msg +viewPageContent appContext project branchRef switchBranch (Paginated history) = + let + branchRef_ = + Maybe.withDefault (Project.defaultBrowsingBranch project) branchRef + + entries = + if List.isEmpty history.items then + [ div [] [ text "No entries" ] ] + + else + List.map (viewHistoryEntry appContext) history.items + in + PageContent.oneColumn + [ div [ class "history-entries" ] entries + , viewPaginationControls project.ref branchRef history + ] + |> PageContent.withPageTitle + (PageTitle.title "History" + |> PageTitle.withRightSide + [ SwitchBranch.toAnchoredOverlay project.ref + branchRef_ + False + switchBranch + |> AnchoredOverlay.map SwitchBranchMsg + |> AnchoredOverlay.view + ] + ) + + +view : + AppContext + -> ProjectDetails + -> Maybe BranchRef + -> Paginated.PageCursorParam + -> Model + -> ( PageLayout Msg, Maybe (Html Msg) ) +view appContext project branchRef _ model = + case model.history of + NotAsked -> + ( viewLoadingPage, Nothing ) + + Loading -> + ( viewLoadingPage, Nothing ) + + Success history -> + ( PageLayout.centeredNarrowLayout + (viewPageContent appContext project branchRef model.switchBranch history) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + , Nothing + ) + + Failure e -> + ( ErrorPage.view appContext.session e "history" "project-history-page" + , Nothing + ) diff --git a/src/UnisonShare/Page/ProjectPage.elm b/src/UnisonShare/Page/ProjectPage.elm index 6c84f969..1261edf3 100644 --- a/src/UnisonShare/Page/ProjectPage.elm +++ b/src/UnisonShare/Page/ProjectPage.elm @@ -40,6 +40,7 @@ import UnisonShare.Page.CodePage as CodePage import UnisonShare.Page.ProjectBranchesPage as ProjectBranchesPage import UnisonShare.Page.ProjectContributionPage as ProjectContributionPage import UnisonShare.Page.ProjectContributionsPage as ProjectContributionsPage +import UnisonShare.Page.ProjectHistoryPage as ProjectHistoryPage import UnisonShare.Page.ProjectOverviewPage as ProjectOverviewPage import UnisonShare.Page.ProjectPageHeader as ProjectPageHeader import UnisonShare.Page.ProjectReleasePage as ProjectReleasePage @@ -48,6 +49,7 @@ import UnisonShare.Page.ProjectSettingsPage as ProjectSettingsPage import UnisonShare.Page.ProjectTicketPage as ProjectTicketPage import UnisonShare.Page.ProjectTicketsPage as ProjectTicketsPage import UnisonShare.PageFooter as PageFooter +import UnisonShare.Paginated as Paginated import UnisonShare.Project as Project exposing (ProjectDetails) import UnisonShare.Project.ProjectRef as ProjectRef exposing (ProjectRef) import UnisonShare.Route as Route exposing (CodeRoute, ProjectRoute(..)) @@ -72,6 +74,7 @@ type SubPage | Contributions ProjectContributionsPage.Model | Ticket TicketRef ProjectTicketPage.Model | Tickets ProjectTicketsPage.Model + | History (Maybe BranchRef) Paginated.PageCursorParam ProjectHistoryPage.Model | Settings ProjectSettingsPage.Model @@ -181,6 +184,9 @@ init appContext projectRef route = in ( Releases releases_, Cmd.map ProjectReleasesPageMsg releasesCmd ) + ProjectHistory branchRef cursor -> + ( History branchRef cursor ProjectHistoryPage.preInit, Cmd.none ) + ProjectSettings -> let settings = @@ -225,6 +231,7 @@ type Msg | ProjectContributionsPageMsg ProjectContributionsPage.Msg | ProjectTicketPageMsg ProjectTicketPage.Msg | ProjectTicketsPageMsg ProjectTicketsPage.Msg + | ProjectHistoryPageMsg ProjectHistoryPage.Msg | ProjectBranchesPageMsg ProjectBranchesPage.Msg | ProjectSettingsPageMsg ProjectSettingsPage.Msg | ChangeRouteTo Route.Route @@ -304,6 +311,15 @@ update appContext projectRef route msg model = ] ) + ( History branchRef cursor _, Success project_ ) -> + let + ( history, cmd ) = + ProjectHistoryPage.init appContext project_ branchRef cursor + in + ( { modelWithProject | subPage = History branchRef cursor history } + , Cmd.map ProjectHistoryPageMsg cmd + ) + _ -> ( modelWithProject, Cmd.none ) @@ -704,6 +720,20 @@ update appContext projectRef route msg model = _ -> ( model, Cmd.none ) + ( History _ _ history, ProjectHistoryPageMsg historyPageMsg ) -> + case ( model.project, route ) of + ( Success project, Route.ProjectHistory branchRef_ cursor ) -> + let + ( history_, historyCmd ) = + ProjectHistoryPage.update appContext project branchRef_ historyPageMsg history + in + ( { model | subPage = History branchRef_ cursor history_ } + , Cmd.map ProjectHistoryPageMsg historyCmd + ) + + _ -> + ( model, Cmd.none ) + ( _, SwitchBranchMsg sbMsg ) -> let ( switchBranch, switchBranchCmd, out ) = @@ -909,6 +939,23 @@ updateSubPage appContext projectRef model route = in ( { model | subPage = Release version releasePage_ }, Cmd.map ProjectReleasePageMsg releasePageCmd ) + ProjectHistory branchRef cursor -> + case model.subPage of + History _ _ _ -> + ( model, Cmd.none ) + + _ -> + case model.project of + Success project -> + let + ( history, cmd ) = + ProjectHistoryPage.init appContext project branchRef cursor + in + ( { model | subPage = History branchRef cursor history }, Cmd.map ProjectHistoryPageMsg cmd ) + + _ -> + ( model, Cmd.none ) + ProjectSettings -> case model.subPage of Settings _ -> @@ -1320,6 +1367,12 @@ viewErrorPage appContext subPage projectRef error = PageFooter.pageFooter |> PageLayout.withSubduedBackground + History _ _ _ -> + PageLayout.centeredNarrowLayout + (PageContent.oneColumn [ errorCard ]) + PageFooter.pageFooter + |> PageLayout.withSubduedBackground + Settings _ -> PageLayout.centeredNarrowLayout (PageContent.oneColumn [ errorCard ]) @@ -1400,6 +1453,9 @@ viewLoadingPage appContext subPage projectRef = , "project-releases-page" ) + History _ _ _ -> + ( ProjectHistoryPage.viewLoadingPage, "project-history-page" ) + Settings _ -> ( ProjectSettingsPage.viewLoadingPage, "project-settings-page" ) in @@ -1477,7 +1533,7 @@ view appContext projectRef model = , switchBranch = AnchoredOverlay.map SwitchBranchMsg - (SwitchBranch.toAnchoredOverlay projectRef branchRef model.switchBranch) + (SwitchBranch.toAnchoredOverlay projectRef branchRef True model.switchBranch) , contextClick = Click.onClick (ChangeRouteTo (Route.projectOverview projectRef)) } project @@ -1714,6 +1770,25 @@ view appContext projectRef model = , modal = modal (Maybe.map (Html.map ProjectTicketsPageMsg) modal_) } + History branchRef cursor history -> + let + ( history_, modal_ ) = + ProjectHistoryPage.view appContext project branchRef cursor history + in + { pageId = "project-page project-history-page" + , title = title + , appHeader = appHeader + , pageHeader = + Just + (pageHeader + ProjectPageHeader.NoActiveNavItem + (Project.defaultBrowsingBranch project) + project + ) + , page = PageLayout.view (PageLayout.map ProjectHistoryPageMsg history_) + , modal = modal (Maybe.map (Html.map ProjectHistoryPageMsg) modal_) + } + Settings settings -> let ( settings_, modal_ ) = diff --git a/src/UnisonShare/Project/BranchHistory.elm b/src/UnisonShare/Project/BranchHistory.elm new file mode 100644 index 00000000..060eb6a6 --- /dev/null +++ b/src/UnisonShare/Project/BranchHistory.elm @@ -0,0 +1,119 @@ +module UnisonShare.Project.BranchHistory exposing (..) + +import Code.FullyQualifiedName as FQN exposing (FQN) +import Code.Hash as Hash exposing (Hash) +import Json.Decode as Decode +import Json.Decode.Pipeline exposing (required, requiredAt) +import Lib.Decode.Helpers exposing (maybeAt, whenTagIs) +import UI.DateTime as DateTime exposing (DateTime) +import UnisonShare.User as User exposing (UserSummary) + + +type CommentAuthor + = VerifiedShareUser UserSummary + | UnverifiedUser { name : String } + + +type HistoryEntry + = Comment CommentDetails + | Changeset ChangesetDetails + + +type alias Change = + { hash : Hash + , fqn : FQN + } + + +type alias CommentDetails = + { createdAt : DateTime + , afterCausalHash : Hash + , author : CommentAuthor + , subject : Maybe String + , body : String + } + + +type alias ChangesetDetails = + { hash : Hash + , updates : List Change + , removes : List Change + , aliases : List Change + , renames : List Change + } + + +type alias BranchHistory = + List HistoryEntry + + + +-- DECODE + + +decodeComment : Decode.Decoder HistoryEntry +decodeComment = + let + decodeAuthor = + Decode.oneOf + [ whenTagIs "ShareUser" (Decode.map VerifiedShareUser User.decodeSummary) + , whenTagIs "UnverifiedUser" + (Decode.map + (\n -> UnverifiedUser { name = n }) + (Decode.field "name" Decode.string) + ) + ] + + toComment createdAt afterCausalHash author subject body = + Comment + { createdAt = createdAt + , afterCausalHash = afterCausalHash + , author = author + , subject = subject + , body = body + } + in + Decode.succeed toComment + |> required "createdAt" DateTime.decode + |> required "afterCausalHash" Hash.decode + |> required "author" decodeAuthor + |> maybeAt [ "comment", "subject" ] Decode.string + |> requiredAt [ "comment", "body" ] Decode.string + + +decodeChangeset : Decode.Decoder HistoryEntry +decodeChangeset = + let + decodeChange = + Decode.succeed Change + |> required "hash" Hash.decode + |> required "fqn" FQN.decode + + toChangeset hash updates removes aliases renames = + Changeset + { hash = hash + , updates = updates + , removes = removes + , aliases = aliases + , renames = renames + } + in + Decode.succeed toChangeset + |> required "hash" Hash.decode + |> required "updates" (Decode.list decodeChange) + |> required "removes" (Decode.list decodeChange) + |> required "aliases" (Decode.list decodeChange) + |> required "renames" (Decode.list decodeChange) + + +decodeHistoryEntry : Decode.Decoder HistoryEntry +decodeHistoryEntry = + Decode.oneOf + [ whenTagIs "Comment" decodeComment + , whenTagIs "Changeset" decodeChangeset + ] + + +decode : Decode.Decoder BranchHistory +decode = + Decode.field "history" (Decode.list decodeHistoryEntry) diff --git a/src/UnisonShare/Route.elm b/src/UnisonShare/Route.elm index 410f0ca0..4c7dfc96 100644 --- a/src/UnisonShare/Route.elm +++ b/src/UnisonShare/Route.elm @@ -41,6 +41,7 @@ module UnisonShare.Route exposing , projectContributions , projectContributionsArchived , projectContributionsMerged + , projectHistory , projectOverview , projectRelease , projectReleases @@ -139,6 +140,7 @@ type ProjectRoute | ProjectContribution ContributionRef ProjectContributionRoute | ProjectContributions ProjectContributionsRoute | ProjectSettings + | ProjectHistory (Maybe BranchRef) PageCursorParam type NotificationsRoute @@ -309,6 +311,11 @@ projectTickets projectRef_ = Project projectRef_ ProjectTickets +projectHistory : ProjectRef -> Maybe BranchRef -> PageCursorParam -> Route +projectHistory projectRef_ branchRef_ cursor = + Project projectRef_ (ProjectHistory branchRef_ cursor) + + projectSettings : ProjectRef -> Route projectSettings projectRef_ = Project projectRef_ ProjectSettings @@ -736,6 +743,20 @@ projectParser queryString = ProjectRef.projectRef handle slug in Project ps ProjectTickets + + projectHistory_ handle slug branchRef = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectHistory (Just branchRef) paginationCursor) + + projectHistoryDefault_ handle slug = + let + ps = + ProjectRef.projectRef handle slug + in + Project ps (ProjectHistory Nothing paginationCursor) in oneOf [ b (succeed projectOverview_ |. slash |= userHandle |. slash |= projectSlug |. end) @@ -755,6 +776,8 @@ projectParser queryString = , b (succeed (projectContributions_ ProjectContributionsArchived) |. slash |= userHandle |. slash |= projectSlug |. slash |. s "contributions" |. slash |. s "archived" |. end) , b (succeed projectTicket_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "tickets" |. slash |= TicketRef.fromUrl |. end) , b (succeed projectTickets_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "tickets" |. end) + , b (succeed projectHistory_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "history" |. slash |= branchRef |. end) + , b (succeed projectHistoryDefault_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "history" |. end) , b (succeed projectSettings_ |. slash |= userHandle |. slash |= projectSlug |. slash |. s "settings" |. end) ] @@ -969,6 +992,12 @@ toUrlPattern r = Project _ ProjectTickets -> ":handle/:project-slug/tickets" + Project _ (ProjectHistory Nothing _) -> + ":handle/:project-slug/history" + + Project _ (ProjectHistory (Just _) _) -> + ":handle/:project-slug/history/:branch-ref" + Project _ ProjectSettings -> ":handle/:project-slug/settings" @@ -1148,6 +1177,12 @@ toUrlString route = Project projectRef_ ProjectTickets -> ( ProjectRef.toUrlPath projectRef_ ++ [ "tickets" ], [] ) + Project projectRef_ (ProjectHistory (Just branchRef_) cursor) -> + ( ProjectRef.toUrlPath projectRef_ ++ "history" :: BranchRef.toUrlPath branchRef_, paginationCursorToQueryParams cursor ) + + Project projectRef_ (ProjectHistory Nothing cursor) -> + ( ProjectRef.toUrlPath projectRef_ ++ [ "history" ], paginationCursorToQueryParams cursor ) + Project projectRef_ ProjectSettings -> ( ProjectRef.toUrlPath projectRef_ ++ [ "settings" ], [] ) diff --git a/src/UnisonShare/SwitchBranch.elm b/src/UnisonShare/SwitchBranch.elm index 66995648..c27ecc87 100644 --- a/src/UnisonShare/SwitchBranch.elm +++ b/src/UnisonShare/SwitchBranch.elm @@ -201,8 +201,8 @@ viewSuggestions data = MaybeE.values [ ownContributorBranches, projectBranches ] -viewSheet : ProjectRef -> Sheet -> Html Msg -viewSheet projectRef sheet = +viewSheet : ProjectRef -> Bool -> Sheet -> Html Msg +viewSheet projectRef withViewAllBranches sheet = let recentBranches = RemoteData.map2 @@ -222,18 +222,25 @@ viewSheet projectRef sheet = { data = recentBranches , view = viewSuggestions } + + viewAll = + if withViewAllBranches then + Just (Link.view "View all branches" (Link.projectBranches projectRef Paginated.NoPageCursor)) + + else + Nothing in Html.map SearchBranchSheetMsg (SearchBranchSheet.view "Switch Branch" suggestions - (Just (Link.view "View all branches" (Link.projectBranches projectRef Paginated.NoPageCursor))) + viewAll sheet.sheet ) -toAnchoredOverlay : ProjectRef -> BranchRef -> Model -> AnchoredOverlay Msg -toAnchoredOverlay projectRef branchRef model = +toAnchoredOverlay : ProjectRef -> BranchRef -> Bool -> Model -> AnchoredOverlay Msg +toAnchoredOverlay projectRef branchRef withViewAllBranches model = let button caret = Button.iconThenLabel ToggleSheet Icon.branch (BranchRef.toString branchRef) @@ -252,4 +259,4 @@ toAnchoredOverlay projectRef branchRef model = Open sheet -> ao_ (button Icon.caretUp) |> AnchoredOverlay.withSheetPosition AnchoredOverlay.BottomLeft - |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet projectRef sheet)) + |> AnchoredOverlay.withSheet (AnchoredOverlay.sheet (viewSheet projectRef withViewAllBranches sheet)) diff --git a/src/css/unison-share/page.css b/src/css/unison-share/page.css index 7f1876ad..111ea54b 100644 --- a/src/css/unison-share/page.css +++ b/src/css/unison-share/page.css @@ -17,6 +17,7 @@ @import "./page/project-contributions-page.css"; @import "./page/project-ticket-page.css"; @import "./page/project-tickets-page.css"; +@import "./page/project-history-page.css"; @import "./page/error-page.css"; @import "./page/cloud-page.css"; @import "./page/ucm-connected.css"; diff --git a/src/css/unison-share/page/project-history-page.css b/src/css/unison-share/page/project-history-page.css new file mode 100644 index 00000000..0d294753 --- /dev/null +++ b/src/css/unison-share/page/project-history-page.css @@ -0,0 +1,142 @@ +.project-history-page { + .history-entries { + display: flex; + flex-direction: column; + gap: 0; + + .history-entry { + display: flex; + flex-direction: row; + --c-history-entry_gap: 1rem; + + &:first-child { + --c-history-entry_gap: 0; + } + + .history-entry_gutter { + width: 2rem; + position: relative; + padding-top: var(--c-history-entry_gap); + + &::before { + position: absolute; + left: calc(0.5625rem - 0.5px); + top: 0; + bottom: 0; + content: " "; + width: 1px; + background: var(--u-color_border_subdued); + z-index: 0; + } + + .icon-wrapper { + position: relative; + z-index: 1; + background: var(--u-color_background_subdued); + height: 1.125rem; + width: 1.125rem; + border-radius: 1rem; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; + } + + .icon { + font-size: var(--font-size-extra-small); + } + + .icon.history-node { + font-size: var(--font-size-small); + } + } + + .history-entry_title-and-details { + padding-top: var(--c-history-entry_gap); + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + + .history-entry_title { + height: 1.125rem; + font-size: var(--font-size-small); + color: var(--u-color_text_subdued); + display: flex; + justify-content: space-between; + align-items: center; + } + + .unverified-user { + font-weight: bold; + } + + .card { + padding: 0.75rem 1rem; + font-size: var(--font-size-medium); + gap: 0; + + h3 { + font-size: var(--font-size-medium); + margin-bottom: 0.25rem; + } + + p:last-child { + margin-bottom: 0; + } + } + + .history-entry_changeset_changes { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 2rem; + font-size: var(--font-size-small); + + .history-entry_changeset_changes_title { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + font-weight: bold; + line-height: 1; + margin-bottom: 0.5rem; + + .icon { + font-size: var(--font-size-small); + color: var(--u-color_icon_subdued); + } + } + + .updates .history-entry_changeset_changes_title label { + color: var(--color-green-1); + } + .removes .history-entry_changeset_changes_title label { + color: var(--color-pink-1); + } + .aliases .history-entry_changeset_changes_title label { + color: var(--color-purple-2); + } + .renames .history-entry_changeset_changes_title label { + color: var(--color-blue-2); + } + + .history-entry_changeset_changes_list { + display: flex; + flex-direction: column; + margin-left: 1rem; + gap: 0.25rem; + + .fully-qualified-name { + font-weight: normal; + height: auto; + } + } + } + } + } + + .history-entry.history-entry_comment { + } + } +}