1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-18 20:59:43 +02:00

Pinned documents

Closes #278

Pin documents to the top of each space.
This commit is contained in:
sauls8t 2020-02-03 21:00:35 +00:00
parent 2b66d0096a
commit e014f5b5c1
18 changed files with 541 additions and 88 deletions

View file

@ -29,16 +29,16 @@ type Store struct {
}
// RecordUserActivity logs user initiated data changes.
func (s Store) RecordUserActivity(ctx domain.RequestContext, activity activity.UserActivity) (err error) {
func (s Store) RecordUserActivity(ctx domain.RequestContext, activity activity.UserActivity) {
activity.OrgID = ctx.OrgID
activity.UserID = ctx.UserID
activity.Created = time.Now().UTC()
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_user_activity (c_orgid, c_userid, c_spaceid, c_docid, c_sectionid, c_sourcetype, c_activitytype, c_metadata, c_created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
_, err := ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_user_activity (c_orgid, c_userid, c_spaceid, c_docid, c_sectionid, c_sourcetype, c_activitytype, c_metadata, c_created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
activity.OrgID, activity.UserID, activity.SpaceID, activity.DocumentID, activity.SectionID, activity.SourceType, activity.ActivityType, activity.Metadata, activity.Created)
if err != nil {
err = errors.Wrap(err, "execute record user activity")
s.Runtime.Log.Error("execute record user activity", err)
}
return

View file

@ -682,7 +682,7 @@ func (b backerHandler) dmzDocument(files *[]backupItem) (err error) {
c_job AS job, c_location AS location, c_name AS name, c_desc AS excerpt, c_slug AS slug,
c_tags AS tags, c_template AS template, c_protection AS protection, c_approval AS approval,
c_lifecycle AS lifecycle, c_versioned AS versioned, c_versionid AS versionid,
c_versionorder AS versionorder, c_groupid AS groupid, c_created AS created, c_revised AS revised
c_versionorder AS versionorder, c_seq AS sequence, c_groupid AS groupid, c_created AS created, c_revised AS revised
FROM dmz_doc`+w)
if err != nil {
return errors.Wrap(err, "select.document")

View file

@ -14,6 +14,7 @@ package document
import (
"database/sql"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"sort"
@ -88,18 +89,12 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: document.SpaceID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
}
@ -190,10 +185,26 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
}
}
// Sort document list by title.
sort.Sort(doc.ByName(filtered))
sortedDocs := doc.SortedDocs{}
response.WriteJSON(w, filtered)
for j := range filtered {
if filtered[j].Sequence == doc.Unsequenced {
sortedDocs.Unpinned = append(sortedDocs.Unpinned, filtered[j])
} else {
sortedDocs.Pinned = append(sortedDocs.Pinned, filtered[j])
}
}
// Sort document list by title.
sort.Sort(doc.ByName(sortedDocs.Unpinned))
// Sort document list by sequence.
sort.Sort(doc.BySeq(sortedDocs.Pinned))
final := sortedDocs.Pinned
final = append(final, sortedDocs.Unpinned...)
response.WriteJSON(w, final)
}
// Update updates an existing document using the format described
@ -249,7 +260,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
}
if oldDoc.SpaceID != d.SpaceID {
h.Store.Category.RemoveDocumentCategories(ctx, d.RefID)
_, _ = h.Store.Category.RemoveDocumentCategories(ctx, d.RefID)
err = h.Store.Document.MoveActivity(ctx, documentID, oldDoc.SpaceID, d.SpaceID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
@ -312,9 +323,9 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
h.Runtime.Commit(ctx.Transaction)
h.Store.Space.SetStats(ctx, d.SpaceID)
_ = h.Store.Space.SetStats(ctx, d.SpaceID)
if oldDoc.SpaceID != d.SpaceID {
h.Store.Space.SetStats(ctx, oldDoc.SpaceID)
_ = h.Store.Space.SetStats(ctx, oldDoc.SpaceID)
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate)
@ -480,18 +491,13 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: "",
DocumentID: "",
Metadata: options.Keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
}
}
@ -526,18 +532,13 @@ func (h *Handler) recordSearchActivity(ctx domain.RequestContext, q []search.Que
}
if _, isExisting := prev[q[i].DocumentID]; !isExisting {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: q[i].SpaceID,
DocumentID: q[i].DocumentID,
Metadata: keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
prev[q[i].DocumentID] = true
}
}
@ -713,16 +714,11 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
}
if document.Lifecycle == workflow.LifecycleLive {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: document.SpaceID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
}
ctx.Transaction.Commit()
@ -786,7 +782,7 @@ func (h *Handler) Export(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(export))
_, _ = w.Write([]byte(export))
}
// Duplicate makes a copy of a document.
@ -1014,3 +1010,219 @@ func (h *Handler) Duplicate(w http.ResponseWriter, r *http.Request) {
response.WriteEmpty(w)
}
// Pin marks existing document with sequence number so that it
// appears at the top-most space view.
func (h *Handler) Pin(w http.ResponseWriter, r *http.Request) {
method := "document.Pin"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
var ok bool
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
if !ok {
h.Runtime.Log.Info("unable to start transaction " + method)
response.WriteServerError(w, method, errors.New("unable to start transaction"))
return
}
d, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if !permission.CanManageSpace(ctx, *h.Store, d.SpaceID) {
h.Runtime.Rollback(ctx.Transaction)
response.WriteForbiddenError(w)
return
}
// Calculate the next sequence number for this newly pinned document.
seq, err := h.Store.Document.PinSequence(ctx, d.SpaceID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
h.Runtime.Log.Error(method, err)
response.WriteServerError(w, method, err)
return
}
err = h.Store.Document.Pin(ctx, documentID, seq+1)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypePinned})
h.Runtime.Commit(ctx.Transaction)
response.WriteEmpty(w)
}
// Unpin removes an existing document from the space pinned list.
func (h *Handler) Unpin(w http.ResponseWriter, r *http.Request) {
method := "document.Unpin"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
var ok bool
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
if !ok {
h.Runtime.Log.Info("unable to start transaction " + method)
response.WriteServerError(w, method, errors.New("unable to start transaction"))
return
}
d, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if !permission.CanManageSpace(ctx, *h.Store, d.SpaceID) {
h.Runtime.Rollback(ctx.Transaction)
response.WriteForbiddenError(w)
return
}
err = h.Store.Document.Unpin(ctx, documentID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeUnpinned})
h.Runtime.Commit(ctx.Transaction)
response.WriteEmpty(w)
}
// PinMove moves pinned document up or down in the sequence.
func (h *Handler) PinMove(w http.ResponseWriter, r *http.Request) {
method := "document.PinMove"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
direction := request.Query(r, "direction")
if len(direction) == 0 {
response.WriteMissingDataError(w, method, "direction")
return
}
var ok bool
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
if !ok {
h.Runtime.Log.Info("unable to start transaction " + method)
response.WriteServerError(w, method, errors.New("unable to start transaction"))
return
}
d, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if !permission.CanManageSpace(ctx, *h.Store, d.SpaceID) {
h.Runtime.Rollback(ctx.Transaction)
response.WriteForbiddenError(w)
return
}
// Get all pinned documents in the space.
pinnedDocs, err := h.Store.Document.Pinned(ctx, d.SpaceID)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
h.Runtime.Log.Error(method, err)
response.WriteServerError(w, method, err)
return
}
// Sort document list by sequence.
sort.Sort(doc.BySeq(pinnedDocs))
// Resequence the documents.
for i := range pinnedDocs {
if pinnedDocs[i].RefID == documentID {
if direction == "u" {
if i-1 >= 0 {
me := pinnedDocs[i].Sequence
target := pinnedDocs[i-1].Sequence
pinnedDocs[i-1].Sequence = me
pinnedDocs[i].Sequence = target
}
}
if direction == "d" {
if i+1 < len(pinnedDocs) {
me := pinnedDocs[i].Sequence
target := pinnedDocs[i+1].Sequence
pinnedDocs[i+1].Sequence = me
pinnedDocs[i].Sequence = target
}
}
break
}
}
// Sort document list by sequence.
sort.Sort(doc.BySeq(pinnedDocs))
// Save the resequenced documents.
for i := range pinnedDocs {
err = h.Store.Document.Pin(ctx, pinnedDocs[i].RefID, i+1)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: d.SpaceID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypePinSequence})
h.Runtime.Commit(ctx.Transaction)
response.WriteEmpty(w)
}

View file

@ -16,10 +16,11 @@ import (
"fmt"
"time"
"github.com/pkg/errors"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store"
"github.com/documize/community/model/doc"
"github.com/pkg/errors"
)
// Store provides data access to space category information.
@ -36,10 +37,12 @@ func (s Store) Add(ctx domain.RequestContext, d doc.Document) (err error) {
_, err = ctx.Transaction.Exec(s.Bind(`
INSERT INTO dmz_doc (c_refid, c_orgid, c_spaceid, c_userid, c_job, c_location, c_name, c_desc, c_slug, c_tags,
c_template, c_protection, c_approval, c_lifecycle, c_versioned, c_versionid, c_versionorder, c_groupid, c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
c_template, c_protection, c_approval, c_lifecycle, c_versioned, c_versionid, c_versionorder, c_seq, c_groupid,
c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
d.RefID, d.OrgID, d.SpaceID, d.UserID, d.Job, d.Location, d.Name, d.Excerpt, d.Slug, d.Tags,
d.Template, d.Protection, d.Approval, d.Lifecycle, d.Versioned, d.VersionID, d.VersionOrder, d.GroupID, d.Created, d.Revised)
d.Template, d.Protection, d.Approval, d.Lifecycle, d.Versioned, d.VersionID, d.VersionOrder, d.Sequence,
d.GroupID, d.Created, d.Revised)
if err != nil {
err = errors.Wrap(err, "execute insert document")
@ -55,7 +58,7 @@ func (s Store) Get(ctx domain.RequestContext, id string) (document doc.Document,
c_job AS job, c_location AS location, c_name AS name, c_desc AS excerpt, c_slug AS slug,
c_tags AS tags, c_template AS template, c_protection AS protection, c_approval AS approval,
c_lifecycle AS lifecycle, c_versioned AS versioned, c_versionid AS versionid,
c_versionorder AS versionorder, c_groupid AS groupid, c_created AS created, c_revised AS revised
c_versionorder AS versionorder, c_seq AS sequence, c_groupid AS groupid, c_created AS created, c_revised AS revised
FROM dmz_doc
WHERE c_orgid=? AND c_refid=?`),
ctx.OrgID, id)
@ -78,7 +81,7 @@ func (s Store) GetBySpace(ctx domain.RequestContext, spaceID string) (documents
c_job AS job, c_location AS location, c_name AS name, c_desc AS excerpt, c_slug AS slug,
c_tags AS tags, c_template AS template, c_protection AS protection, c_approval AS approval,
c_lifecycle AS lifecycle, c_versioned AS versioned, c_versionid AS versionid,
c_versionorder AS versionorder, c_groupid AS groupid, c_created AS created, c_revised AS revised
c_versionorder AS versionorder, c_seq AS sequence, c_groupid AS groupid, c_created AS created, c_revised AS revised
FROM dmz_doc
WHERE c_orgid=? AND c_template=`+s.IsFalse()+` AND c_spaceid IN
(SELECT c_refid FROM dmz_permission WHERE c_orgid=? AND c_location='space' AND c_refid=? AND c_refid IN
@ -111,7 +114,7 @@ func (s Store) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (docu
c_job AS job, c_location AS location, c_name AS name, c_desc AS excerpt, c_slug AS slug,
c_tags AS tags, c_template AS template, c_protection AS protection, c_approval AS approval,
c_lifecycle AS lifecycle, c_versioned AS versioned, c_versionid AS versionid,
c_versionorder AS versionorder, c_groupid AS groupid, c_created AS created, c_revised AS revised
c_versionorder AS versionorder, c_seq AS sequence, c_groupid AS groupid, c_created AS created, c_revised AS revised
FROM dmz_doc
WHERE c_orgid=? AND c_spaceid=? AND c_template=`+s.IsTrue()+` AND c_lifecycle=1
AND c_spaceid IN
@ -167,7 +170,8 @@ func (s Store) Update(ctx domain.RequestContext, document doc.Document) (err err
c_spaceid=:spaceid, c_userid=:userid, c_job=:job, c_location=:location, c_name=:name,
c_desc=:excerpt, c_slug=:slug, c_tags=:tags, c_template=:template,
c_protection=:protection, c_approval=:approval, c_lifecycle=:lifecycle,
c_versioned=:versioned, c_versionid=:versionid, c_versionorder=:versionorder,
c_versioned=:versioned, c_versionid=:versionid, c_versionorder=:versionorder,
c_seq=:sequence,
c_groupid=:groupid, c_revised=:revised
WHERE c_orgid=:orgid AND c_refid=:refid`),
&document)
@ -331,3 +335,78 @@ func (s Store) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.V
return
}
// Pin allocates sequence number to specified document so that it appears
// at the documents list.
func (s Store) Pin(ctx domain.RequestContext, documentID string, seq int) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("UPDATE dmz_doc SET c_seq=? WHERE c_orgid=? AND c_refid=?"),
seq, ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, "document.store.Pin")
}
return
}
// Unpin resets sequence number for given document.
func (s Store) Unpin(ctx domain.RequestContext, documentID string) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("UPDATE dmz_doc SET c_seq=? WHERE c_orgid=? AND c_refid=?"),
doc.Unsequenced, ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, "document.store.Unpin")
}
return
}
// PinSequence fectches pinned documents and returns current
// maximum sequence value.
func (s Store) PinSequence(ctx domain.RequestContext, spaceID string) (max int, err error) {
max = 0
err = s.Runtime.Db.Get(&max, s.Bind(`
SELECT MAX(c_seq)
FROM dmz_doc
WHERE c_orgid=? AND c_spaceid=?
AND c_seq != 99999`),
ctx.OrgID, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
max = doc.Unsequenced
err = errors.Wrap(err, "document.store.PinSequence")
}
return
}
// Pinned documents for space are fetched.
func (s Store) Pinned(ctx domain.RequestContext, spaceID string) (d []doc.Document, err error) {
d = []doc.Document{}
err = s.Runtime.Db.Select(&d, s.Bind(`
SELECT id, c_refid AS refid, c_orgid AS orgid, c_spaceid AS spaceid, c_userid AS userid,
c_job AS job, c_location AS location, c_name AS name, c_desc AS excerpt, c_slug AS slug,
c_tags AS tags, c_template AS template, c_protection AS protection, c_approval AS approval,
c_lifecycle AS lifecycle, c_versioned AS versioned, c_versionid AS versionid,
c_versionorder AS versionorder, c_seq AS sequence, c_groupid AS groupid,
c_created AS created, c_revised AS revised
FROM dmz_doc
WHERE c_orgid=? AND c_spaceid=?
AND c_seq != 99999
ORDER BY c_seq`),
ctx.OrgID, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, "document.store.Pinned")
}
return
}

View file

@ -1504,18 +1504,13 @@ func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
if err != nil {
h.Runtime.Log.Error(method, err)
} else {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: doc.SpaceID,
DocumentID: doc.RefID,
Metadata: source, // deliberate
SourceType: activity.SourceTypeSearch, // deliberate
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
}
}

View file

@ -138,14 +138,10 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: sp.RefID,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeCreated})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
@ -444,16 +440,11 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: sp.RefID,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
response.WriteJSON(w, sp)
@ -657,14 +648,10 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: id,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeDeleted})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
@ -764,14 +751,10 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SpaceID: id,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeDeleted})
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
}
h.Runtime.Commit(ctx.Transaction)

View file

@ -190,6 +190,10 @@ type DocumentStorer interface {
DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error)
GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error)
MoveActivity(ctx domain.RequestContext, documentID, oldSpaceID, newSpaceID string) (err error)
Pin(ctx domain.RequestContext, documentID string, seq int) (err error)
Unpin(ctx domain.RequestContext, documentID string) (err error)
PinSequence(ctx domain.RequestContext, spaceID string) (max int, err error)
Pinned(ctx domain.RequestContext, spaceID string) (d []doc.Document, err error)
}
// SettingStorer defines required methods for persisting global and user level settings
@ -228,7 +232,7 @@ type LinkStorer interface {
// ActivityStorer defines required methods for persisting document activity
type ActivityStorer interface {
RecordUserActivity(ctx domain.RequestContext, activity activity.UserActivity) (err error)
RecordUserActivity(ctx domain.RequestContext, activity activity.UserActivity)
GetDocumentActivity(ctx domain.RequestContext, id string) (a []activity.DocumentActivity, err error)
DeleteDocumentChangeActivity(ctx domain.RequestContext, id string) (rows int64, err error)
}

View file

@ -31,6 +31,9 @@ export default Component.extend({
hasDocumentActions: computed('permissions.{documentDelete,documentMove}', function() {
return this.get('permissions.documentDelete') || this.get('permissions.documentMove');
}),
hasCategoryFilter: computed('categoryFilter', function() {
return !_.isEmpty(this.get('categoryFilter'));
}),
didReceiveAttrs() {
this._super(...arguments);
@ -53,7 +56,7 @@ export default Component.extend({
let viewDensity = this.get('localStorage').getSessionItem('space.density');
if (!_.isNull(viewDensity) && !_.isUndefined(viewDensity)) {
this.set('viewDensity', viewDensity);
}
}
},
actions: {
@ -86,7 +89,7 @@ export default Component.extend({
},
// eslint-disable-next-line no-unused-vars
onSortBy(attacher) {
onSortBy(attacher) {
// attacher.hide();
this.get('onFiltered')(this.get('documents'));
},
@ -161,6 +164,18 @@ export default Component.extend({
this.set('selectedCaption', list.length > 1 ? 'documents' : 'document');
this.set('selectedDocuments', A(list));
}
},
onPin(documentId) {
this.get('onPin')(documentId);
},
onUnpin(documentId) {
this.get('onUnpin')(documentId);
},
onPinSequence(documentId, direction) {
this.get('onPinSequence')(documentId, direction);
},
}
});

View file

@ -36,7 +36,7 @@ export default Component.extend(AuthMixin, {
this.setup();
},
didReceiveAttrs() {
didUpdateAttrs() {
this._super(...arguments);
this.setup();
},
@ -138,6 +138,7 @@ export default Component.extend(AuthMixin, {
this.set('selectedFilter', filter);
this.set('categories', categories);
this.get('onFiltered')(filtered);
}
}

View file

@ -15,6 +15,8 @@ import EmberObject from "@ember/object";
// let constants = this.get('constants');
let constants = EmberObject.extend({
Unsequenced: 99999,
SpaceType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Public: 1,
Private: 2,

View file

@ -30,6 +30,7 @@ export default Model.extend({
versioned: attr('boolean'),
versionId: attr('string'),
versionOrder: attr('number', { defaultValue: 0 }),
sequence: attr('number', { defaultValue: 99999 }),
groupId: attr('string'),
created: attr(),
revised: attr(),
@ -46,12 +47,12 @@ export default Model.extend({
isDraft: computed('lifecycle', function () {
let constants = this.get('constants');
return this.get('lifecycle') == constants.Lifecycle.Draft;
return this.get('lifecycle') === constants.Lifecycle.Draft;
}),
isLive: computed('lifecycle', function () {
let constants = this.get('constants');
return this.get('lifecycle') == constants.Lifecycle.Live;
return this.get('lifecycle') === constants.Lifecycle.Live;
}),
lifecycleLabel: computed('lifecycle', function () {
@ -77,5 +78,10 @@ export default Model.extend({
let after = moment().subtract(7, 'days');
return moment(this.get('revised')).isSameOrAfter(after) &&
moment(this.get('created')).isBefore(after);
})
}),
isSequenced: computed('sequence', function () {
let constants = this.get('constants');
return this.get('sequence') !== constants.Unsequenced;
}),
});

View file

@ -95,29 +95,54 @@ export default Controller.extend(NotifierMixin, {
onFiltered(docs) {
let ls = this.get('localStorage');
let sortBy = this.get('sortBy');
let constants = this.get('constants');
if (_.isNull(docs)) return;
let pinned = _.filter(docs, function(d) { return d.get('sequence') !== constants.Unsequenced; })
let unpinned = _.filter(docs, function(d) { return d.get('sequence') === constants.Unsequenced; })
if (sortBy.name) {
docs = docs.sortBy('name');
unpinned = unpinned.sortBy('name');
ls.storeSessionItem('space.sortBy', 'name');
}
if (sortBy.created) {
docs = docs.sortBy('created');
unpinned = unpinned.sortBy('created');
ls.storeSessionItem('space.sortBy', 'created');
}
if (sortBy.updated) {
docs = docs.sortBy('revised');
unpinned = unpinned.sortBy('revised');
ls.storeSessionItem('space.sortBy', 'updated');
}
if (sortBy.desc) {
docs = docs.reverseObjects();
unpinned = unpinned.reverseObjects();
ls.storeSessionItem('space.sortOrder', 'desc');
} else {
ls.storeSessionItem('space.sortOrder', 'asc');
}
this.set('filteredDocs', docs);
}
this.set('filteredDocs', _.concat(pinned, unpinned));
},
onPin(documentId) {
this.get('documentSvc').pin(documentId).then(() => {
this.notifySuccess('Pinned');
this.send('onRefresh');
});
},
onUnpin(documentId) {
this.get('documentSvc').unpin(documentId).then(() => {
this.notifySuccess('Unpinned');
this.send('onRefresh');
});
},
onPinSequence(documentId, direction) {
this.get('documentSvc').onPinSequence(documentId, direction).then(() => {
this.notifySuccess('Moved');
this.send('onRefresh');
});
},
}
});

View file

@ -71,7 +71,11 @@
space=model.folder
templates=model.templates
permissions=model.permissions
categoryFilter=category
sortBy=sortBy
onPin=(action "onPin")
onUnpin=(action "onUnpin")
onPinSequence=(action "onPinSequence")
onFiltered=(action "onFiltered")
onExportDocument=(action "onExportDocument")
onDeleteDocument=(action "onDeleteDocument")

View file

@ -567,7 +567,37 @@ export default Service.extend({
}).catch((error) => {
return error;
});
}
},
//**************************************************
// Pinning documents inside spaces.
//**************************************************
// Pin document
pin(documentId) {
return this.get('ajax').request(`document/pin/${documentId}`, {
method: 'POST'
}).then((response) => {
return response;
});
},
// Unpin document
unpin(documentId) {
return this.get('ajax').request(`document/unpin/${documentId}`, {
method: 'DELETE'
}).then((response) => {
return response;
});
},
onPinSequence(documentId, direction) {
return this.get('ajax').request(`document/pinmove/${documentId}?direction=${direction}`, {
method: 'POST'
}).then((response) => {
return response;
});
}
});
function isObject(a) {

View file

@ -34,6 +34,10 @@
> .checkbox {
display: block;
}
> .sequence {
display: block;
}
}
> .checkbox {
@ -51,6 +55,21 @@
}
}
> .sequence {
position: absolute;
display: none;
top: 10px;
right: 40px;
cursor: pointer;
> .dicon {
color: map-get($yellow-shades, 700);
cursor: pointer;
font-weight: 600;
font-size: 20px;
}
}
> .actions {
position: absolute;
display: none;
@ -68,6 +87,17 @@
font-size: 1.3rem;
font-weight: 700;
color: map-get($gray-shades, 800);
> .pinned {
display: inline-block;
margin-left: 10px;
> .dicon {
font-size: 0.9rem;
font-weight: 500;
color: map-get($gray-shades, 600);
}
}
}
> .desc {
@ -168,6 +198,10 @@
display: block;
}
> .sequence {
display: block;
}
> .actions {
display: block;
}

View file

@ -63,13 +63,22 @@
{{#each documents key="id" as |document|}}
<li class="document {{if document.selected "selected"}}" id="document-{{document.id}}">
{{#link-to "document.index" space.id space.slug document.id document.slug class="info"}}
<div class="name">{{ document.name }}</div>
<div class="name">
{{ document.name }}
{{#if document.isSequenced}}
<div class="pinned">
<i class="dicon {{constants.Icon.TickDouble}}">
{{#attach-tooltip showDelay=250}}Pinned{{/attach-tooltip}}
</i>
</div>
{{/if}}
</div>
{{#if (not-eq viewDensity "3")}}
<div class="desc">{{ document.excerpt }}</div>
{{/if}}
{{#if (eq viewDensity "1")}}
<div class="meta">
<div class="lifecycle">
<div class="lifecycle">
<div class="{{if (eq document.lifecycle constants.Lifecycle.Draft) "draft"}}
{{if (eq document.lifecycle constants.Lifecycle.Live) "live"}}
{{if (eq document.lifecycle constants.Lifecycle.Archived) "archived"}}">
@ -83,6 +92,25 @@
{{/link-to}}
{{#if hasDocumentActions}}
<div class="sequence">
{{#if document.isSequenced}}
{{#unless hasCategoryFilter}}
<i class="dicon {{constants.Icon.ArrowSmallUp}}" {{action "onPinSequence" document.id "u"}}>
{{#attach-tooltip showDelay=250}}Move up{{/attach-tooltip}}
</i>
<i class="dicon {{constants.Icon.ArrowSmallDown}}" {{action "onPinSequence" document.id "d"}}>
{{#attach-tooltip showDelay=250}}Move down{{/attach-tooltip}}
</i>
{{/unless}}
<i class="dicon {{constants.Icon.Cross}}" {{action "onUnpin" document.id}}>
{{#attach-tooltip showDelay=250}}Unpin{{/attach-tooltip}}
</i>
{{else}}
<i class="dicon {{constants.Icon.ArrowSmallUp}}" {{action "onPin" document.id}}>
{{#attach-tooltip showDelay=250}}Pin{{/attach-tooltip}}
</i>
{{/if}}
</div>
<div class="checkbox" {{action "selectDocument" document.id}}>
{{#if document.selected}}
<i class="dicon {{constants.Icon.CheckboxChecked}}"/>

View file

@ -115,6 +115,15 @@ const (
// TypePublished happens when a document is moved from Draft to Live.
TypePublished Type = 16
// TypePinned happens when a document is pinned within space.
TypePinned Type = 17
// TypeUnpinned happens when a document is no longer pinned inside a space.
TypeUnpinned Type = 18
// TypePinSequence is when the order of sequenced documents is changed.
TypePinSequence Type = 19
)
// TypeName returns one-work descriptor for activity type
@ -152,6 +161,12 @@ func TypeName(t Type) string {
return "Search"
case TypePublished:
return "Publish"
case TypePinned:
return "Pinned"
case TypeUnpinned:
return "Unpinned"
case TypePinSequence:
return "Sequence"
}
return ""

View file

@ -38,6 +38,7 @@ type Document struct {
Versioned bool `json:"versioned"`
VersionID string `json:"versionId"`
VersionOrder int `json:"versionOrder"`
Sequence int `json:"sequence"`
GroupID string `json:"groupId"`
// Read-only presentation only data
@ -67,6 +68,13 @@ func (a ByID) Len() int { return len(a) }
func (a ByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByID) Less(i, j int) bool { return a[i].RefID > a[j].RefID }
// BySeq sorts a collection of documents by sequenced number.
type BySeq []Document
func (a BySeq) Len() int { return len(a) }
func (a BySeq) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a BySeq) Less(i, j int) bool { return a[i].Sequence < a[j].Sequence }
// DocumentMeta details who viewed the document.
type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"`
@ -118,3 +126,15 @@ type DuplicateModel struct {
DocumentID string `json:"documentId"`
Name string `json:"documentName"`
}
// SortedDocs provides list od pinned and unpinned documents
// sorted by sequence and name respectively.
type SortedDocs struct {
Pinned []Document `json:"pinned"`
Unpinned []Document `json:"unpinned"`
}
const (
// Unsequenced tells us if document is pinned or not
Unsequenced int = 99999
)