Skip to content

Commit 7353a80

Browse files
authored
Merge pull request #129 from unisoncomputing/project-history-page
Add initial ProjectHistoryPage
2 parents abb4c42 + 2966e2b commit 7353a80

File tree

10 files changed

+762
-8
lines changed

10 files changed

+762
-8
lines changed

src/UnisonShare/Api.elm

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,18 @@ projectBranchReleaseNotes projectRef branchRef =
429429
}
430430

431431

432+
projectBranchHistory : ProjectRef -> BranchRef -> Endpoint
433+
projectBranchHistory projectRef branchRef =
434+
let
435+
( handle, slug ) =
436+
ProjectRef.toApiStringParts projectRef
437+
in
438+
GET
439+
{ path = [ "users", handle, "projects", slug, "history", BranchRef.toApiUrlString branchRef ]
440+
, queryParams = []
441+
}
442+
443+
432444

433445
-- PROJECT ROLE ASSIGNMENTS (COLLABORATORS)
434446

src/UnisonShare/Link.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ projectOverview projectRef_ =
251251
toClick (Route.projectOverview projectRef_)
252252

253253

254+
projectHistory : ProjectRef -> Maybe BranchRef -> PageCursorParam -> Click msg
255+
projectHistory projectRef_ branchRef_ cursor =
256+
toClick (Route.projectHistory projectRef_ branchRef_ cursor)
257+
258+
254259
projectBranches : ProjectRef -> PageCursorParam -> Click msg
255260
projectBranches projectRef_ cursor =
256261
toClick (Route.projectBranches projectRef_ cursor)
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
module UnisonShare.Page.ProjectHistoryPage exposing (..)
2+
3+
import Code.BranchRef exposing (BranchRef)
4+
import Code.FullyQualifiedName as FQN
5+
import Code.Hash as Hash
6+
import Html exposing (Html, div, h3, label, p, span, text)
7+
import Html.Attributes exposing (class)
8+
import Lib.Util as Util
9+
import RemoteData exposing (RemoteData(..), WebData)
10+
import String.Extra exposing (pluralize)
11+
import UI
12+
import UI.AnchoredOverlay as AnchoredOverlay
13+
import UI.Card as Card
14+
import UI.DateTime as DateTime
15+
import UI.Icon as Icon exposing (Icon)
16+
import UI.PageContent as PageContent exposing (PageContent)
17+
import UI.PageLayout as PageLayout exposing (PageLayout)
18+
import UI.PageTitle as PageTitle
19+
import UI.Placeholder as Placeholder
20+
import UI.ProfileSnippet as ProfileSnippet
21+
import UnisonShare.AppContext exposing (AppContext)
22+
import UnisonShare.Link as Link
23+
import UnisonShare.Page.ErrorPage as ErrorPage
24+
import UnisonShare.PageFooter as PageFooter
25+
import UnisonShare.Paginated as Paginated exposing (Paginated(..))
26+
import UnisonShare.Project as Project exposing (ProjectDetails)
27+
import UnisonShare.Project.BranchHistory as BranchHistory exposing (HistoryEntry)
28+
import UnisonShare.Project.ProjectRef exposing (ProjectRef)
29+
import UnisonShare.Route as Route
30+
import UnisonShare.SwitchBranch as SwitchBranch
31+
32+
33+
34+
-- MODEL
35+
36+
37+
type alias PaginatedBranchHistory =
38+
Paginated.Paginated HistoryEntry
39+
40+
41+
type alias Model =
42+
{ history : WebData PaginatedBranchHistory
43+
, switchBranch : SwitchBranch.Model
44+
}
45+
46+
47+
preInit : Model
48+
preInit =
49+
{ history = NotAsked
50+
, switchBranch = SwitchBranch.init
51+
}
52+
53+
54+
init : AppContext -> ProjectDetails -> Maybe BranchRef -> Paginated.PageCursorParam -> ( Model, Cmd Msg )
55+
init appContext project branchRef cursor =
56+
let
57+
branchRef_ =
58+
Maybe.withDefault (Project.defaultBrowsingBranch project) branchRef
59+
in
60+
( { history = Loading
61+
, switchBranch = SwitchBranch.init
62+
}
63+
, fetchProjectBranchHistory appContext project.ref branchRef_ cursor
64+
)
65+
66+
67+
68+
-- UPDATE
69+
70+
71+
type Msg
72+
= FetchProjectBranchHistoryFinished BranchRef (WebData PaginatedBranchHistory)
73+
| SwitchBranchMsg SwitchBranch.Msg
74+
75+
76+
update : AppContext -> ProjectDetails -> Maybe BranchRef -> Msg -> Model -> ( Model, Cmd Msg )
77+
update appContext project _ msg model =
78+
case msg of
79+
FetchProjectBranchHistoryFinished _ history ->
80+
( { model | history = history }, Cmd.none )
81+
82+
SwitchBranchMsg sbMsg ->
83+
let
84+
( switchBranch, switchBranchCmd, out ) =
85+
SwitchBranch.update appContext project.ref sbMsg model.switchBranch
86+
87+
navCmd =
88+
case out of
89+
SwitchBranch.SwitchToBranchRequest branchRef ->
90+
Route.navigate
91+
appContext.navKey
92+
(Route.projectHistory
93+
project.ref
94+
(Just branchRef)
95+
Paginated.NoPageCursor
96+
)
97+
98+
_ ->
99+
Cmd.none
100+
in
101+
( { model | switchBranch = switchBranch }
102+
, Cmd.batch
103+
[ Cmd.map SwitchBranchMsg switchBranchCmd
104+
, navCmd
105+
]
106+
)
107+
108+
109+
110+
-- EFFECTS
111+
112+
113+
fetchProjectBranchHistory : AppContext -> ProjectRef -> BranchRef -> Paginated.PageCursorParam -> Cmd Msg
114+
fetchProjectBranchHistory _ _ branchRef _ =
115+
let
116+
{-
117+
params =
118+
{ limit = 64
119+
, cursor = cursor
120+
}
121+
-}
122+
mkPaginated prev next items =
123+
Paginated.Paginated { prev = prev, next = next, items = items }
124+
125+
mock =
126+
RemoteData.Success
127+
(mkPaginated
128+
Nothing
129+
(Just (Paginated.PageCursor "asdf-1234"))
130+
[ BranchHistory.Comment
131+
{ createdAt = DateTime.unsafeFromISO8601 "2025-11-03T13:35:33-05:00"
132+
, afterCausalHash = Hash.unsafeFromString "commenthash1"
133+
, author = BranchHistory.UnverifiedUser { name = "Simon" }
134+
, subject = Just "Just fixed a bunch of stuff"
135+
, body = "And it was really cool"
136+
}
137+
, BranchHistory.Changeset
138+
{ causalHash = Hash.unsafeFromString "namespacehash3"
139+
, updates =
140+
[ { hash = Hash.unsafeFromString "definitionhash1"
141+
, fqn = FQN.fromString "List.map"
142+
}
143+
]
144+
, removes = []
145+
, aliases =
146+
[ { hash = Hash.unsafeFromString "definitionhash1"
147+
, fqn = FQN.fromString "List.map"
148+
}
149+
]
150+
, renames = []
151+
}
152+
, BranchHistory.Comment
153+
{ createdAt = DateTime.unsafeFromISO8601 "2025-11-03T13:35:33-05:00"
154+
, afterCausalHash = Hash.unsafeFromString "commenthash1"
155+
, author = BranchHistory.UnverifiedUser { name = "Simon" }
156+
, subject = Just "Just fixed a bunch of stuff"
157+
, body = "And it was really cool"
158+
}
159+
]
160+
)
161+
in
162+
Util.delayMsg 500 (FetchProjectBranchHistoryFinished branchRef mock)
163+
164+
165+
166+
{-
167+
ShareApi.projectBranchHistory projectRef branchRef
168+
|> HttpApi.toRequest
169+
BranchHistory.decode
170+
(RemoteData.fromResult >> FetchProjectBranchHistoryFinished branchRef)
171+
|> HttpApi.perform appContext.api
172+
-}
173+
-- VIEW
174+
175+
176+
viewLoadingPage : PageLayout msg
177+
viewLoadingPage =
178+
let
179+
shape length =
180+
Placeholder.text
181+
|> Placeholder.withLength length
182+
|> Placeholder.subdued
183+
|> Placeholder.tiny
184+
|> Placeholder.view
185+
186+
content =
187+
PageContent.oneColumn
188+
[ Card.card
189+
[ shape Placeholder.Large
190+
, shape Placeholder.Small
191+
, shape Placeholder.Medium
192+
]
193+
|> Card.asContained
194+
|> Card.view
195+
]
196+
|> PageContent.withPageTitle (PageTitle.title "History")
197+
in
198+
PageLayout.centeredNarrowLayout content PageFooter.pageFooter
199+
|> PageLayout.withSubduedBackground
200+
201+
202+
viewHistoryEntry_ : String -> Icon msg -> { leftTitle : Html msg, rightTitle : Html msg } -> List (Html msg) -> Html msg
203+
viewHistoryEntry_ className icon { leftTitle, rightTitle } cardContent =
204+
div [ class ("history-entry " ++ className) ]
205+
[ div [ class "history-entry_gutter" ]
206+
[ div [ class "icon-wrapper" ] [ Icon.view icon ]
207+
]
208+
, div [ class "history-entry_title-and-details" ]
209+
[ div [ class "history-entry_title" ]
210+
[ leftTitle
211+
, rightTitle
212+
]
213+
, Card.card cardContent
214+
|> Card.withClassName "project-history_entry"
215+
|> Card.asContained
216+
|> Card.view
217+
]
218+
]
219+
220+
221+
viewHistoryEntry : AppContext -> HistoryEntry -> Html msg
222+
viewHistoryEntry appContext entry =
223+
case entry of
224+
BranchHistory.Comment comment ->
225+
let
226+
createdAt =
227+
DateTime.view
228+
(DateTime.DistanceFrom appContext.now)
229+
appContext.timeZone
230+
comment.createdAt
231+
232+
author =
233+
case comment.author of
234+
BranchHistory.VerifiedShareUser user ->
235+
ProfileSnippet.view (ProfileSnippet.profileSnippet user)
236+
237+
BranchHistory.UnverifiedUser { name } ->
238+
span [ class "unverified-user" ] [ text name ]
239+
in
240+
viewHistoryEntry_
241+
"history-entry_comment"
242+
Icon.tag
243+
{ leftTitle = author, rightTitle = createdAt }
244+
[ comment.subject
245+
|> Maybe.map (\s -> h3 [] [ text s ])
246+
|> Maybe.withDefault UI.nothing
247+
, p [] [ text comment.body ]
248+
]
249+
250+
BranchHistory.Changeset changeset ->
251+
let
252+
numChanges =
253+
(changeset.updates ++ changeset.removes ++ changeset.aliases ++ changeset.renames)
254+
|> List.length
255+
|> pluralize "change" "changes"
256+
257+
viewChanges : String -> Icon msg -> String -> List BranchHistory.Change -> Html msg
258+
viewChanges className icon title changes =
259+
if List.isEmpty changes then
260+
UI.nothing
261+
262+
else
263+
div [ class className ]
264+
[ div [ class "history-entry_changeset_changes_title" ]
265+
[ Icon.view icon, label [] [ text title ] ]
266+
, div [ class "history-entry_changeset_changes_list" ]
267+
(List.map (.fqn >> FQN.view) changes)
268+
]
269+
270+
viewCardContent : BranchHistory.ChangesetDetails -> Html msg
271+
viewCardContent { updates, removes, aliases, renames } =
272+
div [ class "history-entry_changeset_changes" ]
273+
[ viewChanges "updates" Icon.largePlus "Adds/Updates" updates
274+
, viewChanges "removes" Icon.trash "Removes" removes
275+
, viewChanges "aliases" Icon.tags "Aliases" aliases
276+
, viewChanges "renames" Icon.tag "Renames" renames
277+
]
278+
in
279+
viewHistoryEntry_
280+
"history-entry_changeset"
281+
Icon.historyNode
282+
{ leftTitle = Hash.view changeset.causalHash, rightTitle = text numChanges }
283+
[ viewCardContent changeset ]
284+
285+
286+
viewPaginationControls : ProjectRef -> Maybe BranchRef -> { a | prev : Maybe Paginated.PageCursor, next : Maybe Paginated.PageCursor } -> Html msg
287+
viewPaginationControls projectRef branchRef cursors =
288+
let
289+
toLink cursor =
290+
Link.projectHistory projectRef branchRef cursor
291+
in
292+
Paginated.view toLink cursors
293+
294+
295+
viewPageContent : AppContext -> ProjectDetails -> Maybe BranchRef -> SwitchBranch.Model -> PaginatedBranchHistory -> PageContent Msg
296+
viewPageContent appContext project branchRef switchBranch (Paginated history) =
297+
let
298+
branchRef_ =
299+
Maybe.withDefault (Project.defaultBrowsingBranch project) branchRef
300+
301+
entries =
302+
if List.isEmpty history.items then
303+
[ div [] [ text "No entries" ] ]
304+
305+
else
306+
List.map (viewHistoryEntry appContext) history.items
307+
in
308+
PageContent.oneColumn
309+
[ div [ class "history-entries" ] entries
310+
, viewPaginationControls project.ref branchRef history
311+
]
312+
|> PageContent.withPageTitle
313+
(PageTitle.title "History"
314+
|> PageTitle.withRightSide
315+
[ SwitchBranch.toAnchoredOverlay project.ref
316+
branchRef_
317+
False
318+
switchBranch
319+
|> AnchoredOverlay.map SwitchBranchMsg
320+
|> AnchoredOverlay.view
321+
]
322+
)
323+
324+
325+
view :
326+
AppContext
327+
-> ProjectDetails
328+
-> Maybe BranchRef
329+
-> Paginated.PageCursorParam
330+
-> Model
331+
-> ( PageLayout Msg, Maybe (Html Msg) )
332+
view appContext project branchRef _ model =
333+
case model.history of
334+
NotAsked ->
335+
( viewLoadingPage, Nothing )
336+
337+
Loading ->
338+
( viewLoadingPage, Nothing )
339+
340+
Success history ->
341+
( PageLayout.centeredNarrowLayout
342+
(viewPageContent appContext project branchRef model.switchBranch history)
343+
PageFooter.pageFooter
344+
|> PageLayout.withSubduedBackground
345+
, Nothing
346+
)
347+
348+
Failure e ->
349+
( ErrorPage.view appContext.session e "history" "project-history-page"
350+
, Nothing
351+
)

0 commit comments

Comments
 (0)