@@ -77,23 +77,23 @@ func New(service changes.Service, users users.Service, opt Options) http.Handler
handler: h.ServeHTTP,
users: users,
}
}
// RepoSpecContextKey is a context key for the request's issues.RepoSpec.
// That value specifies which repo the issues are to be displayed for.
// RepoSpecContextKey is a context key for the request's repo spec.
// That value specifies which repo the changes are to be displayed for.
// The associated value will be of type string.
var RepoSpecContextKey = &contextKey{"RepoSpec"}
// BaseURIContextKey is a context key for the request's base URI.
// That value specifies the base URI prefix to use for all absolute URLs.
// The associated value will be of type string.
var BaseURIContextKey = &contextKey{"BaseURI"}
// Options for configuring issues app.
// Options for configuring changes app.
type Options struct {
Notifications notifications.Service // If not nil, issues containing unread notifications are highlighted.
Notifications notifications.Service // If not nil, changes containing unread notifications are highlighted.
DisableReactions bool // Disable all support for displaying and toggling reactions.
HeadPre, HeadPost template.HTML
BodyPre string // An html/template definition of "body-pre" template.
@@ -144,18 +144,18 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
return nil
}
// Handle "/".
if req.URL.Path == "/" {
return h.IssuesHandler(w, req)
return h.ChangesHandler(w, req)
}
// Handle "/{changeID}" and "/{changeID}/...".
elems := strings.SplitN(req.URL.Path[1:], "/", 3)
changeID, err := strconv.ParseUint(elems[0], 10, 64)
if err != nil {
return httperror.HTTP{Code: http.StatusNotFound, Err: fmt.Errorf("invalid issue ID %q: %v", elems[0], err)}
return httperror.HTTP{Code: http.StatusNotFound, Err: fmt.Errorf("invalid change ID %q: %v", elems[0], err)}
}
switch {
// "/{changeID}".
case len(elems) == 1:
return h.ChangeHandler(w, req, changeID)
@@ -176,11 +176,11 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) error {
default:
return httperror.HTTP{Code: http.StatusNotFound, Err: errors.New("no route")}
}
}
func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error {
func (h *handler) ChangesHandler(w http.ResponseWriter, req *http.Request) error {
if req.Method != http.MethodGet {
return httperror.Method{Allowed: []string{http.MethodGet}}
}
state, err := h.state(req, 0)
if err != nil {
@@ -194,22 +194,22 @@ func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error
if err != nil {
return err
}
openCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{State: changes.StateFilter(changes.OpenState)})
if err != nil {
return fmt.Errorf("issues.Count(open): %v", err)
return fmt.Errorf("changes.Count(open): %v", err)
}
closedCount, err := h.is.Count(req.Context(), state.RepoSpec, changes.ListOptions{State: changes.StateFilter(changes.ClosedState)})
if err != nil {
return fmt.Errorf("issues.Count(closed): %v", err)
return fmt.Errorf("changes.Count(closed): %v", err)
}
var es []component.ChangeEntry
for _, i := range is {
es = append(es, component.ChangeEntry{Change: i, BaseURI: state.BaseURI})
}
es = state.augmentUnread(req.Context(), es, h.is, h.Notifications)
state.Changes = component.Issues{
state.Changes = component.Changes{
IssuesNav: component.IssuesNav{
OpenCount: openCount,
ClosedCount: closedCount,
Path: state.BaseURI + state.ReqPath,
Query: req.URL.Query(),
@@ -217,23 +217,23 @@ func (h *handler) IssuesHandler(w http.ResponseWriter, req *http.Request) error
},
Filter: filter,
Entries: es,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = h.static.ExecuteTemplate(w, "issues.html.tmpl", &state)
err = h.static.ExecuteTemplate(w, "changes.html.tmpl", &state)
if err != nil {
return fmt.Errorf("h.static.ExecuteTemplate: %v", err)
}
return nil
}
const (
// stateQueryKey is name of query key for controlling issue state filter.
// stateQueryKey is name of query key for controlling change state filter.
stateQueryKey = "state"
)
// stateFilter parses the issue state filter from query,
// stateFilter parses the change state filter from query,
// returning an error if the value is unsupported.
func stateFilter(query url.Values) (changes.StateFilter, error) {
selectedTabName := query.Get(stateQueryKey)
switch selectedTabName {
case "":
@@ -296,38 +296,38 @@ func (h *handler) ChangeHandler(w http.ResponseWriter, req *http.Request, change
}
state, err := h.state(req, changeID)
if err != nil {
return err
}
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
if err != nil {
return err
}
cs, err := h.is.ListComments(req.Context(), state.RepoSpec, state.IssueID, nil)
cs, err := h.is.ListComments(req.Context(), state.RepoSpec, state.ChangeID, nil)
if err != nil {
return fmt.Errorf("changes.ListComments: %v", err)
}
es, err := h.is.ListEvents(req.Context(), state.RepoSpec, state.IssueID, nil)
es, err := h.is.ListEvents(req.Context(), state.RepoSpec, state.ChangeID, nil)
if err != nil {
return fmt.Errorf("changes.ListEvents: %v", err)
}
var items []issueItem
var items []timelineItem
for _, comment := range cs {
items = append(items, issueItem{comment})
items = append(items, timelineItem{comment})
}
for _, event := range es {
items = append(items, issueItem{event})
items = append(items, timelineItem{event})
}
sort.Sort(byCreatedAtID(items))
state.Items = items
// Call loadTemplates to set updated reactionsBar, reactableID, etc., template functions.
t, err := loadTemplates(state.State, h.Options.BodyPre)
if err != nil {
return fmt.Errorf("loadTemplates: %v", err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = t.ExecuteTemplate(w, "issue.html.tmpl", &state)
err = t.ExecuteTemplate(w, "change.html.tmpl", &state)
if err != nil {
return fmt.Errorf("t.ExecuteTemplate: %v", err)
}
return nil
}
@@ -338,15 +338,15 @@ func (h *handler) ChangeCommitsHandler(w http.ResponseWriter, req *http.Request,
}
state, err := h.state(req, changeID)
if err != nil {
return err
}
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
if err != nil {
return err
}
cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.IssueID)
cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.ChangeID)
if err != nil {
return err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err = h.static.ExecuteTemplate(w, "change-commits.html.tmpl", &state)
@@ -373,17 +373,17 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
}
state, err := h.state(req, changeID)
if err != nil {
return err
}
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.IssueID)
state.Change, err = h.is.Get(req.Context(), state.RepoSpec, state.ChangeID)
if err != nil {
return err
}
var commit commitMessage
if commitID != "" {
cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.IssueID)
cs, err := h.is.ListCommits(req.Context(), state.RepoSpec, state.ChangeID)
if err != nil {
return err
}
i := commitIndex(cs, commitID)
if i == -1 {
@@ -406,11 +406,11 @@ func (h *handler) ChangeFilesHandler(w http.ResponseWriter, req *http.Request, c
}
var opt *changes.GetDiffOptions
if commitID != "" {
opt = &changes.GetDiffOptions{Commit: commitID}
}
rawDiff, err := h.is.GetDiff(req.Context(), state.RepoSpec, state.IssueID, opt)
rawDiff, err := h.is.GetDiff(req.Context(), state.RepoSpec, state.ChangeID, opt)
if err != nil {
return err
}
fileDiffs, err := diff.ParseMultiFileDiff(rawDiff)
if err != nil {
@@ -452,18 +452,18 @@ func (h *handler) state(req *http.Request, changeID uint64) (state, error) {
// TODO: Caller still does a lot of work outside to calculate req.URL.Path by
// subtracting BaseURI from full original req.URL.Path. We should be able
// to compute it here internally by using req.RequestURI and BaseURI.
reqPath := req.URL.Path
if reqPath == "/" {
reqPath = "" // This is needed so that absolute URL for root view, i.e., /issues, is "/issues" and not "/issues/" because of "/issues" + "/".
reqPath = "" // This is needed so that absolute URL for root view, i.e., /changes, is "/changes" and not "/changes/" because of "/changes" + "/".
}
b := state{
State: common.State{
BaseURI: req.Context().Value(BaseURIContextKey).(string),
ReqPath: reqPath,
RepoSpec: req.Context().Value(RepoSpecContextKey).(string),
IssueID: changeID,
ChangeID: changeID,
},
}
b.HeadPre = h.HeadPre
b.HeadPost = h.HeadPost
if h.BodyTop != nil {
@@ -498,35 +498,35 @@ type state struct {
HeadPre, HeadPost template.HTML
BodyTop template.HTML
common.State
Changes component.Issues
Changes component.Changes
Change changes.Change
Items []issueItem
Items []timelineItem
}
func (s state) Tabnav(selected string) template.HTML {
// Render the tabnav.
return template.HTML(htmlg.RenderComponentsString(tabnav{
Tabs: []tab{
{
Content: iconText{Icon: octiconssvg.CommentDiscussion, Text: "Discussion"},
URL: fmt.Sprintf("%s/%d", s.BaseURI, s.IssueID),
URL: fmt.Sprintf("%s/%d", s.BaseURI, s.ChangeID),
Selected: selected == "Discussion",
},
{
Content: contentCounter{
Content: iconText{Icon: octiconssvg.GitCommit, Text: "Commits"},
Count: s.Change.Commits,
},
URL: fmt.Sprintf("%s/%d/commits", s.BaseURI, s.IssueID),
URL: fmt.Sprintf("%s/%d/commits", s.BaseURI, s.ChangeID),
Selected: selected == "Commits",
},
{
Content: iconText{Icon: octiconssvg.Diff, Text: "Files"},
URL: fmt.Sprintf("%s/%d/files", s.BaseURI, s.IssueID),
URL: fmt.Sprintf("%s/%d/files", s.BaseURI, s.ChangeID),
Selected: selected == "Files",
},
},
}))
}
@@ -546,11 +546,11 @@ func loadTemplates(state common.State, bodyPre string) (*template.Template, erro
"reactionPosition": func(emojiID reactions.EmojiID) string { return reactions.Position(":" + string(emojiID) + ":") },
"equalUsers": func(a, b users.User) bool {
return a.UserSpec == b.UserSpec
},
"reactableID": func(commentID uint64) string {
return fmt.Sprintf("%d/%d", state.IssueID, commentID)
return fmt.Sprintf("%d/%d", state.ChangeID, commentID)
},
"reactionsBar": func(reactions []reactions.Reaction, reactableID string) htmlg.Component {
return reactionscomponent.ReactionsBar{
Reactions: reactions,
CurrentUser: state.CurrentUser,