mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
Merge pull request #356 from writefreely/draft-list-paging
Draft list paging
This commit is contained in:
commit
73450a50e3
7 changed files with 151 additions and 11 deletions
19
account.go
19
account.go
|
@ -16,6 +16,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -691,6 +692,22 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
|
|||
return ErrBadRequestedType
|
||||
}
|
||||
|
||||
isAnonPosts := r.FormValue("anonymous") == "1"
|
||||
if isAnonPosts {
|
||||
pageStr := r.FormValue("page")
|
||||
pg, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
log.Error("Error parsing page parameter '%s': %s", pageStr, err)
|
||||
pg = 1
|
||||
}
|
||||
|
||||
p, err := app.db.GetAnonymousPosts(u, pg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||
}
|
||||
|
||||
var err error
|
||||
p := GetPostsCache(u.ID)
|
||||
if p == nil {
|
||||
|
@ -731,7 +748,7 @@ func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
|||
}
|
||||
|
||||
func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||
p, err := app.db.GetAnonymousPosts(u)
|
||||
p, err := app.db.GetAnonymousPosts(u, 1)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch anon posts: %v", err)
|
||||
}
|
||||
|
|
17
database.go
17
database.go
|
@ -77,7 +77,7 @@ type writestore interface {
|
|||
GetTotalCollections() (int64, error)
|
||||
GetTotalPosts() (int64, error)
|
||||
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
||||
GetAnonymousPosts(u *User) (*[]PublicPost, error)
|
||||
GetAnonymousPosts(u *User, page int) (*[]PublicPost, error)
|
||||
GetUserPosts(u *User) (*[]PublicPost, error)
|
||||
|
||||
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error)
|
||||
|
@ -1806,8 +1806,19 @@ func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
|||
return &posts, nil
|
||||
}
|
||||
|
||||
func (db *datastore) GetAnonymousPosts(u *User) (*[]PublicPost, error) {
|
||||
rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC", u.ID)
|
||||
func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) {
|
||||
pagePosts := 10
|
||||
start := page*pagePosts - pagePosts
|
||||
if page == 0 {
|
||||
start = 0
|
||||
pagePosts = 1000
|
||||
}
|
||||
|
||||
limitStr := ""
|
||||
if page > 0 {
|
||||
limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts)
|
||||
}
|
||||
rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID)
|
||||
if err != nil {
|
||||
log.Error("Failed selecting from posts: %v", err)
|
||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}
|
||||
|
|
|
@ -110,7 +110,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
|
|||
log.Error("unable to fetch collections: %v", err)
|
||||
}
|
||||
|
||||
posts, err := app.db.GetAnonymousPosts(u)
|
||||
posts, err := app.db.GetAnonymousPosts(u, 0)
|
||||
if err != nil {
|
||||
log.Error("unable to fetch anon posts: %v", err)
|
||||
}
|
||||
|
|
20
handle.go
20
handle.go
|
@ -287,6 +287,26 @@ func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
|
|||
return h.UserAll(false, f, apiAuth)
|
||||
}
|
||||
|
||||
// UserWebAPI handles endpoints that accept a user authorized either via the web (cookies) or an Authorization header.
|
||||
func (h *Handler) UserWebAPI(f userHandlerFunc) http.HandlerFunc {
|
||||
return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
|
||||
// Authorize user via cookies
|
||||
u := getUserSession(app, r)
|
||||
if u != nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Fall back to access token, since user isn't logged in via web
|
||||
var err error
|
||||
u, err = apiAuth(app, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
handleFunc := func() error {
|
||||
|
|
|
@ -115,7 +115,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
|||
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
|
||||
apiMe := write.PathPrefix("/api/me/").Subrouter()
|
||||
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
|
||||
apiMe.HandleFunc("/posts", handler.UserAPI(viewMyPostsAPI)).Methods("GET")
|
||||
apiMe.HandleFunc("/posts", handler.UserWebAPI(viewMyPostsAPI)).Methods("GET")
|
||||
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
|
||||
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
|
||||
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
|
||||
|
|
|
@ -181,9 +181,17 @@ var localPosts = function() {
|
|||
undoDelete: UndoDelete,
|
||||
};
|
||||
}();
|
||||
var createPostEl = function(post) {
|
||||
var movePostHTML = function(postID) {
|
||||
let $tmpl = document.getElementById('move-tmpl');
|
||||
if ($tmpl === null) {
|
||||
return "";
|
||||
}
|
||||
return $tmpl.innerHTML.replace(/POST_ID/g, postID);
|
||||
}
|
||||
var createPostEl = function(post, owned) {
|
||||
var $post = document.createElement('div');
|
||||
var title = (post.title || post.id);
|
||||
let p = H.createPost(post.id, "", post.body)
|
||||
var title = (post.title || p.title || post.id);
|
||||
title = title.replace(/</g, "<");
|
||||
$post.id = 'post-' + post.id;
|
||||
$post.className = 'post';
|
||||
|
@ -194,13 +202,22 @@ var createPostEl = function(post) {
|
|||
posted = getFormattedDate(new Date(post.created))
|
||||
}
|
||||
var hasDraft = H.exists('draft' + post.id);
|
||||
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\')">delete</a></h4>';
|
||||
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\'' + (owned === true ? ', true' : '') + ')">delete</a> '+movePostHTML(post.id)+'</h4>';
|
||||
|
||||
if (post.error) {
|
||||
$post.innerHTML += '<p class="error"><strong>Sync error:</strong> ' + post.error + ' <nav><a href="#" onclick="localPosts.dismissError(event, this)">dismiss</a> <a href="#" onclick="localPosts.deletePost(event, this, \''+post.id+'\')">remove post</a></nav></p>';
|
||||
}
|
||||
if (post.summary) {
|
||||
// TODO: switch to using p.summary, after ensuring it matches summary generated on the backend.
|
||||
$post.innerHTML += '<p>' + post.summary.replace(/</g, "<") + '</p>';
|
||||
} else if (post.body) {
|
||||
var preview;
|
||||
if (post.body.length > 140) {
|
||||
preview = post.body.substr(0, 140) + '...';
|
||||
} else {
|
||||
preview = post.body;
|
||||
}
|
||||
$post.innerHTML += '<p>' + preview.replace(/</g, "<") + '</p>';
|
||||
}
|
||||
return $post;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
{{define "articles"}}
|
||||
{{template "header" .}}
|
||||
<style type="text/css">
|
||||
a.loading {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
#move-tmpl {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="snug content-container">
|
||||
|
||||
|
@ -15,7 +24,7 @@
|
|||
{{ if .AnonymousPosts }}
|
||||
<p>These are your draft posts. You can share them individually (without a blog) or move them to your blog when you're ready.</p>
|
||||
|
||||
<div class="atoms posts">
|
||||
<div id="anon-posts" class="atoms posts">
|
||||
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
|
||||
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
|
||||
<h4>
|
||||
|
@ -39,9 +48,12 @@
|
|||
</h4>
|
||||
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{end}}
|
||||
</div>{{end}}
|
||||
</div>{{ else }}<div id="no-posts-published">
|
||||
</div>
|
||||
{{if eq (len .AnonymousPosts) 10}}<p id="load-more-p"><a href="#load">Load more...</a></p>{{end}}
|
||||
{{ else }}<div id="no-posts-published">
|
||||
<p>Your anonymous and draft posts will show up here once you've published some. You'll be able to share them individually (without a blog) or move them to a blog when you're ready.</p>
|
||||
{{if not .SingleUser}}<p>Alternatively, see your blogs and their posts on your <a href="/me/c/">Blogs</a> page.</p>{{end}}
|
||||
|
||||
<p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }}
|
||||
|
||||
<div id="moving"></div>
|
||||
|
@ -52,6 +64,25 @@
|
|||
|
||||
</div>
|
||||
|
||||
{{ if .Collections }}
|
||||
<div id="move-tmpl">
|
||||
{{if gt (len .Collections) 1}}
|
||||
<div class="action flat-select">
|
||||
<select id="move-POST_ID" onchange="postActions.multiMove(this, 'POST_ID', {{if .SingleUser}}true{{else}}false{{end}})" title="Move this post to one of your blogs">
|
||||
<option style="display:none"></option>
|
||||
{{range .Collections}}<option value="{{.Alias}}">{{.DisplayTitle}}</option>{{end}}
|
||||
</select>
|
||||
<label for="move-POST_ID">move to...</label>
|
||||
<img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
|
||||
</div>
|
||||
{{else}}
|
||||
{{range .Collections}}
|
||||
<a class="action" href="/POST_ID" title="Publish this post to your blog '{{.DisplayTitle}}'" onclick="postActions.move(this, 'POST_ID', '{{.Alias}}', {{if $.SingleUser}}true{{else}}false{{end}});return false">move to {{.DisplayTitle}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<script src="/js/h.js"></script>
|
||||
<script src="/js/postactions.js"></script>
|
||||
<script>
|
||||
|
@ -145,6 +176,50 @@ function postsLoaded(n) {
|
|||
syncing = true;
|
||||
});
|
||||
}
|
||||
|
||||
var $loadMore = H.getEl("load-more-p");
|
||||
var curPage = 1;
|
||||
var isLoadingMore = false;
|
||||
function loadMorePosts() {
|
||||
if (isLoadingMore === true) {
|
||||
return;
|
||||
}
|
||||
var $link = this;
|
||||
isLoadingMore = true;
|
||||
|
||||
$link.className = 'loading';
|
||||
$link.textContent = 'Loading posts...';
|
||||
|
||||
var $posts = H.getEl("anon-posts");
|
||||
|
||||
curPage++;
|
||||
|
||||
var http = new XMLHttpRequest();
|
||||
var url = "/api/me/posts?anonymous=1&page=" + curPage;
|
||||
http.open("GET", url, true);
|
||||
http.setRequestHeader("Content-type", "application/json");
|
||||
http.onreadystatechange = function() {
|
||||
if (http.readyState == 4) {
|
||||
if (http.status == 200) {
|
||||
var data = JSON.parse(http.responseText);
|
||||
for (var i=0; i<data.data.length; i++) {
|
||||
$posts.el.appendChild(createPostEl(data.data[i], true));
|
||||
}
|
||||
if (data.data.length < 10) {
|
||||
$loadMore.el.parentNode.removeChild($loadMore.el);
|
||||
}
|
||||
} else {
|
||||
alert("Failed to load more posts. Please try again.");
|
||||
curPage--;
|
||||
}
|
||||
isLoadingMore = false;
|
||||
$link.className = '';
|
||||
$link.textContent = 'Load more...';
|
||||
}
|
||||
}
|
||||
http.send();
|
||||
}
|
||||
$loadMore.el.querySelector('a').addEventListener('click', loadMorePosts);
|
||||
</script>
|
||||
<script src="/js/posts.js"></script>
|
||||
|
||||
|
|
Loading…
Reference in a new issue