mirror of
https://github.com/documize/community.git
synced 2025-07-23 23:29:42 +02:00
commit
6738d2c9e1
52 changed files with 1521 additions and 982 deletions
16
README.md
16
README.md
|
@ -1,9 +1,3 @@
|
|||
> *We provide frequent product updates for both cloud and self-hosted customers.*
|
||||
>
|
||||
> **Harvey Kandola / CEO & Founder / Documize, Inc.**
|
||||
|
||||
## Welcome
|
||||
|
||||
Documize is an open source modern, lightweight and comprehensive alternative to Confluence.
|
||||
|
||||
It's built with Golang + EmberJS and compiled down to a single executable binary for Linux, Windows and macOS.
|
||||
|
@ -19,9 +13,13 @@ All you need to provide is PostgreSQL or any MySQL variant.
|
|||
|
||||
## Latest Release
|
||||
|
||||
[Community Edition: v2.1.1](https://github.com/documize/community/releases)
|
||||
[Community Edition: v2.2.0](https://github.com/documize/community/releases)
|
||||
|
||||
[Enterprise Edition: v2.1.1](https://www.documize.com/downloads)
|
||||
[Enterprise Edition: v2.2.0](https://www.documize.com/downloads)
|
||||
|
||||
> *We provide frequent product updates for both cloud and self-hosted customers.*
|
||||
>
|
||||
> **Harvey Kandola, CEO/Founder, Documize, Inc.**
|
||||
|
||||
## OS support
|
||||
|
||||
|
@ -50,7 +48,7 @@ Heck, Documize will probably run just fine on a Raspberry Pi 3.
|
|||
|
||||
## Technology Stack
|
||||
|
||||
- Go (v1.12.0)
|
||||
- Go (v1.12.1)
|
||||
- Ember JS (v3.8.0)
|
||||
|
||||
## Authentication Options
|
||||
|
|
59
core/env/runtime.go
vendored
59
core/env/runtime.go
vendored
|
@ -13,25 +13,25 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const (
|
||||
// SiteModeNormal serves app
|
||||
SiteModeNormal = ""
|
||||
// SiteModeNormal serves app
|
||||
SiteModeNormal = ""
|
||||
|
||||
// SiteModeOffline serves offline.html
|
||||
SiteModeOffline = "1"
|
||||
// SiteModeOffline serves offline.html
|
||||
SiteModeOffline = "1"
|
||||
|
||||
// SiteModeSetup tells Ember to serve setup route
|
||||
SiteModeSetup = "2"
|
||||
// SiteModeSetup tells Ember to serve setup route
|
||||
SiteModeSetup = "2"
|
||||
|
||||
// SiteModeBadDB redirects to db-error.html page
|
||||
SiteModeBadDB = "3"
|
||||
// SiteModeBadDB redirects to db-error.html page
|
||||
SiteModeBadDB = "3"
|
||||
)
|
||||
|
||||
// Runtime provides access to database, logger and other server-level scoped objects.
|
||||
|
@ -44,40 +44,37 @@ type Runtime struct {
|
|||
Product domain.Product
|
||||
}
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
// StartTx beings database transaction with application defined
|
||||
// database transaction isolation level.
|
||||
// Any error encountered during this operation is logged to runtime logger.
|
||||
func (r *Runtime) StartTx() (tx *sqlx.Tx, ok bool) {
|
||||
tx, err := r.Db.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelReadUncommitted})
|
||||
if err != nil {
|
||||
r.Log.Error("unable to start database transaction", err)
|
||||
return nil, false
|
||||
}
|
||||
func (r *Runtime) StartTx(i sql.IsolationLevel) (tx *sqlx.Tx, ok bool) {
|
||||
tx, err := r.Db.BeginTxx(context.Background(), &sql.TxOptions{Isolation: i})
|
||||
if err != nil {
|
||||
r.Log.Error("unable to start database transaction", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return tx, true
|
||||
return tx, true
|
||||
}
|
||||
|
||||
// Rollback aborts active database transaction.
|
||||
// Any error encountered during this operation is logged to runtime logger.
|
||||
func (r *Runtime) Rollback(tx *sqlx.Tx) bool {
|
||||
if err := tx.Commit(); err != nil {
|
||||
r.Log.Error("unable to commit database transaction", err)
|
||||
return false
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
r.Log.Error("unable to commit database transaction", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
// Commit flushes pending changes to database.
|
||||
// Any error encountered during this operation is logged to runtime logger.
|
||||
func (r *Runtime) Commit(tx *sqlx.Tx) bool {
|
||||
if err := tx.Commit(); err != nil {
|
||||
r.Log.Error("unable to commit database transaction", err)
|
||||
return false
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
r.Log.Error("unable to commit database transaction", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
|||
// Get attachment being requested.
|
||||
a, err := h.Store.Attachment.GetAttachment(ctx, ctx.OrgID, request.Param(r, "attachmentID"))
|
||||
if err == sql.ErrNoRows {
|
||||
response.WriteNotFoundError(w, method, request.Param(r, "fileID"))
|
||||
response.WriteNotFoundError(w, method, request.Param(r, "attachmentID"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -161,6 +161,12 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
|
|||
canDownload = true
|
||||
}
|
||||
|
||||
if len(secureToken) == 0 && len(authToken) == 0 {
|
||||
h.Runtime.Log.Error("get attachment received no access token", err)
|
||||
response.WriteForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Send back error if caller unable view attachment
|
||||
if !canDownload {
|
||||
h.Runtime.Log.Error("get attachment refused", err)
|
||||
|
|
|
@ -273,6 +273,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
|||
// Check for required fields.
|
||||
if len(username) == 0 || len(password) == 0 {
|
||||
response.WriteUnauthorizedError(w)
|
||||
h.Runtime.Log.Info("LDAP authentication aborted due to missing username/password")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -313,6 +314,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
if !ok {
|
||||
h.Runtime.Log.Info("LDAP failed login request for " + username + " @ " + dom)
|
||||
response.WriteUnauthorizedError(w)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -179,6 +179,18 @@ func (s Store) Update(ctx domain.RequestContext, document doc.Document) (err err
|
|||
return
|
||||
}
|
||||
|
||||
// UpdateRevised sets document revision date to UTC now.
|
||||
func (s Store) UpdateRevised(ctx domain.RequestContext, docID string) (err error) {
|
||||
_, err = ctx.Transaction.Exec(s.Bind(`UPDATE dmz_doc SET c_revised=? WHERE c_orgid=? AND c_refid=?`),
|
||||
time.Now().UTC(), ctx.OrgID, docID)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "document.store.UpdateRevised")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateGroup applies same values to all documents with the same group ID.
|
||||
func (s Store) UpdateGroup(ctx domain.RequestContext, d doc.Document) (err error) {
|
||||
_, err = ctx.Transaction.Exec(s.Bind(`UPDATE dmz_doc SET c_name=?, c_desc=? WHERE c_orgid=? AND c_groupid=?`),
|
||||
|
|
|
@ -175,6 +175,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
|||
ActivityType: activity.TypeCreated})
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd)
|
||||
|
@ -492,6 +495,9 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, model.Page.DocumentID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
if doc.Lifecycle == workflow.LifecycleLive {
|
||||
|
@ -593,6 +599,9 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
h.Store.Page.DeletePageRevisions(ctx, pageID)
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
|
||||
|
@ -702,6 +711,9 @@ func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
|
||||
|
@ -779,6 +791,9 @@ func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)
|
||||
|
@ -848,6 +863,9 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionResequence)
|
||||
|
@ -969,6 +987,9 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
|
|||
ActivityType: activity.TypeCreated})
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, targetID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionCopy)
|
||||
|
@ -1223,6 +1244,9 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
|
|||
ActivityType: activity.TypeReverted})
|
||||
}
|
||||
|
||||
// Update doc revised.
|
||||
h.Store.Document.UpdateRevised(ctx, doc.RefID)
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
||||
h.Store.Audit.Record(ctx, audit.EventTypeSectionRollback)
|
||||
|
|
|
@ -12,12 +12,14 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/documize/community/domain"
|
||||
"github.com/documize/community/model/attachment"
|
||||
"github.com/documize/community/model/category"
|
||||
"github.com/documize/community/model/doc"
|
||||
"github.com/documize/community/model/page"
|
||||
sm "github.com/documize/community/model/search"
|
||||
"github.com/documize/community/model/workflow"
|
||||
)
|
||||
|
||||
// IndexDocument adds search indesd entries for document inserting title, tags and attachments as
|
||||
|
@ -26,20 +28,21 @@ func (m *Indexer) IndexDocument(ctx domain.RequestContext, d doc.Document, a []a
|
|||
method := "search.IndexDocument"
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = m.runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
m.runtime.Log.Error(method, err)
|
||||
ok := true
|
||||
ctx.Transaction, ok = m.runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
m.runtime.Log.Info("unable to start TX for " + method)
|
||||
return
|
||||
}
|
||||
|
||||
err = m.store.Search.IndexDocument(ctx, d, a)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
m.runtime.Rollback(ctx.Transaction)
|
||||
m.runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
m.runtime.Commit(ctx.Transaction)
|
||||
}
|
||||
|
||||
// DeleteDocument removes all search entries for document.
|
||||
|
@ -47,20 +50,21 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
|
|||
method := "search.DeleteDocument"
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = m.runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
m.runtime.Log.Error(method, err)
|
||||
ok := true
|
||||
ctx.Transaction, ok = m.runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
m.runtime.Log.Info("unable to start TX for " + method)
|
||||
return
|
||||
}
|
||||
|
||||
err = m.store.Search.DeleteDocument(ctx, ID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
m.runtime.Rollback(ctx.Transaction)
|
||||
m.runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
m.runtime.Commit(ctx.Transaction)
|
||||
}
|
||||
|
||||
// IndexContent adds search index entry for document context.
|
||||
|
@ -69,25 +73,26 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
|
|||
method := "search.IndexContent"
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = m.runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
m.runtime.Log.Error(method, err)
|
||||
// we do not index pending pages
|
||||
if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew {
|
||||
return
|
||||
}
|
||||
|
||||
ok := true
|
||||
ctx.Transaction, ok = m.runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
m.runtime.Log.Info("unable to start TX for " + method)
|
||||
return
|
||||
}
|
||||
|
||||
err = m.store.Search.IndexContent(ctx, p)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
m.runtime.Rollback(ctx.Transaction)
|
||||
m.runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.Transaction.Commit()
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
m.runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
m.runtime.Commit(ctx.Transaction)
|
||||
}
|
||||
|
||||
// DeleteContent removes all search entries for specific document content.
|
||||
|
@ -95,20 +100,21 @@ func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) {
|
|||
method := "search.DeleteContent"
|
||||
var err error
|
||||
|
||||
ctx.Transaction, err = m.runtime.Db.Beginx()
|
||||
if err != nil {
|
||||
m.runtime.Log.Error(method, err)
|
||||
ok := true
|
||||
ctx.Transaction, ok = m.runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
m.runtime.Log.Info("unable to start TX for " + method)
|
||||
return
|
||||
}
|
||||
|
||||
err = m.store.Search.DeleteContent(ctx, pageID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
m.runtime.Rollback(ctx.Transaction)
|
||||
m.runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
m.runtime.Commit(ctx.Transaction)
|
||||
}
|
||||
|
||||
// FilterCategoryProtected removes search results that cannot be seen by user
|
||||
|
|
|
@ -24,7 +24,6 @@ import (
|
|||
"github.com/documize/community/model/doc"
|
||||
"github.com/documize/community/model/page"
|
||||
"github.com/documize/community/model/search"
|
||||
"github.com/documize/community/model/workflow"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -125,11 +124,6 @@ func (s Store) DeleteDocument(ctx domain.RequestContext, ID string) (err error)
|
|||
func (s Store) IndexContent(ctx domain.RequestContext, p page.Page) (err error) {
|
||||
method := "search.IndexContent"
|
||||
|
||||
// we do not index pending pages
|
||||
if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew {
|
||||
return
|
||||
}
|
||||
|
||||
// remove previous search entries
|
||||
_, err = ctx.Transaction.Exec(s.Bind("DELETE FROM dmz_search WHERE c_orgid=? AND c_docid=? AND c_itemid=? AND c_itemtype='page'"),
|
||||
ctx.OrgID, p.DocumentID, p.RefID)
|
||||
|
@ -289,7 +283,7 @@ func (s Store) matchFullText(ctx domain.RequestContext, keywords, itemType strin
|
|||
s.id, s.c_orgid AS orgid, s.c_docid AS documentid, s.c_itemid AS itemid, s.c_itemtype AS itemtype,
|
||||
d.c_spaceid as spaceid, COALESCE(d.c_name,'Unknown') AS document, d.c_tags AS tags,
|
||||
d.c_desc AS excerpt, d.c_template AS template, d.c_versionid AS versionid,
|
||||
COALESCE(l.c_name,'Unknown') AS space
|
||||
COALESCE(l.c_name,'Unknown') AS space, d.c_created AS created, d.c_revised AS revised
|
||||
FROM
|
||||
dmz_search s,
|
||||
dmz_doc d
|
||||
|
@ -343,7 +337,7 @@ func (s Store) matchLike(ctx domain.RequestContext, keywords, itemType string) (
|
|||
sql1 := s.Bind(`SELECT
|
||||
s.id, s.c_orgid AS orgid, s.c_docid AS documentid, s.c_itemid AS itemid, s.c_itemtype AS itemtype,
|
||||
d.c_spaceid as spaceid, COALESCE(d.c_name,'Unknown') AS document, d.c_tags AS tags, d.c_desc AS excerpt,
|
||||
COALESCE(l.c_name,'Unknown') AS space
|
||||
COALESCE(l.c_name,'Unknown') AS space, d.c_created AS created, d.c_revised AS revised
|
||||
FROM
|
||||
dmz_search s,
|
||||
dmz_doc d
|
||||
|
|
|
@ -36,7 +36,6 @@ func Register(rt *env.Runtime, s *store.Store) {
|
|||
provider.Register("code", &code.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("jira", &jira.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("gemini", &gemini.Provider{Runtime: rt, Store: s})
|
||||
// provider.Register("github", &github.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("markdown", &markdown.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("papertrail", &papertrail.Provider{Runtime: rt, Store: s})
|
||||
provider.Register("tabular", &tabular.Provider{Runtime: rt, Store: s})
|
||||
|
|
|
@ -696,7 +696,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Delete the space first.
|
||||
ok := true
|
||||
ctx.Transaction, ok = h.Runtime.StartTx()
|
||||
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
response.WriteError(w, method)
|
||||
return
|
||||
|
@ -712,7 +712,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
h.Runtime.Commit(ctx.Transaction)
|
||||
|
||||
// Delete data associated with this space.
|
||||
ctx.Transaction, ok = h.Runtime.StartTx()
|
||||
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
response.WriteError(w, method)
|
||||
return
|
||||
|
@ -756,7 +756,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
h.Runtime.Commit(ctx.Transaction)
|
||||
|
||||
// Record this action.
|
||||
ctx.Transaction, ok = h.Runtime.StartTx()
|
||||
ctx.Transaction, ok = h.Runtime.StartTx(sql.LevelReadUncommitted)
|
||||
if !ok {
|
||||
response.WriteError(w, method)
|
||||
return
|
||||
|
|
|
@ -184,6 +184,7 @@ type DocumentStorer interface {
|
|||
TemplatesBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error)
|
||||
PublicDocuments(ctx domain.RequestContext, orgID string) (documents []doc.SitemapDocument, err error)
|
||||
Update(ctx domain.RequestContext, document doc.Document) (err error)
|
||||
UpdateRevised(ctx domain.RequestContext, docID string) (err error)
|
||||
UpdateGroup(ctx domain.RequestContext, document doc.Document) (err error)
|
||||
ChangeDocumentSpace(ctx domain.RequestContext, document, space string) (err error)
|
||||
MoveDocumentSpace(ctx domain.RequestContext, id, move string) (err error)
|
||||
|
|
|
@ -40,9 +40,9 @@ func main() {
|
|||
// product details
|
||||
rt.Product = domain.Product{}
|
||||
rt.Product.Major = "2"
|
||||
rt.Product.Minor = "1"
|
||||
rt.Product.Patch = "1"
|
||||
rt.Product.Revision = "190304203624"
|
||||
rt.Product.Minor = "2"
|
||||
rt.Product.Patch = "0"
|
||||
rt.Product.Revision = "190313174432"
|
||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||
rt.Product.Edition = domain.CommunityEdition
|
||||
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
||||
|
|
1576
embed/bindata.go
1576
embed/bindata.go
File diff suppressed because one or more lines are too long
|
@ -63,5 +63,6 @@ module.exports = {
|
|||
"slug": true,
|
||||
"iziToast": true,
|
||||
"Papa": true,
|
||||
"Popper": true,
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ module.exports = {
|
|||
'no-invalid-interactive': false,
|
||||
'no-nested-interactive': false,
|
||||
'no-triple-curlies': false,
|
||||
'no-global-jquery': false,
|
||||
'style-concatenation': false,
|
||||
'simple-unless': false,
|
||||
}
|
||||
|
|
|
@ -119,6 +119,8 @@ export default Component.extend(Notifier, Modals, {
|
|||
meta: meta
|
||||
};
|
||||
|
||||
this.set('newSectionName', '');
|
||||
|
||||
const promise = this.addSection(model);
|
||||
promise.then((id) => {
|
||||
this.set('toEdit', model.page.pageType === 'section' ? id : '');
|
||||
|
@ -152,6 +154,8 @@ export default Component.extend(Notifier, Modals, {
|
|||
meta: meta
|
||||
};
|
||||
|
||||
this.set('newSectionName', '');
|
||||
|
||||
const promise = this.addSection(model);
|
||||
promise.then((id) => { // eslint-disable-line no-unused-vars
|
||||
});
|
||||
|
|
|
@ -9,15 +9,18 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import { computed } from '@ember/object';
|
||||
import { A } from '@ember/array';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
localStorage: service(),
|
||||
showDeleteDialog: false,
|
||||
showMoveDialog: false,
|
||||
selectedDocuments: A([]),
|
||||
selectedCaption: 'document',
|
||||
viewDensity: "1",
|
||||
|
||||
showAdd: computed('permissions.documentAdd', 'documents', function() {
|
||||
return this.get('documents.length') === 0 && this.get('permissions.documentAdd');
|
||||
|
@ -36,9 +39,63 @@ export default Component.extend({
|
|||
let targets = _.reject(this.get('spaces'), {id: space.get('id')});
|
||||
this.set('moveOptions', A(targets));
|
||||
this.set('selectedDocuments', A([]));
|
||||
|
||||
let sortBy = this.get('localStorage').getSessionItem('space.sortBy');
|
||||
if (!_.isNull(sortBy) && !_.isUndefined(sortBy)) {
|
||||
this.send('onSetSort', sortBy);
|
||||
}
|
||||
|
||||
let sortOrder = this.get('localStorage').getSessionItem('space.sortOrder');
|
||||
if (!_.isNull(sortOrder) && !_.isUndefined(sortOrder)) {
|
||||
this.send('onSetSort', sortOrder);
|
||||
}
|
||||
|
||||
let viewDensity = this.get('localStorage').getSessionItem('space.density');
|
||||
if (!_.isNull(viewDensity) && !_.isUndefined(viewDensity)) {
|
||||
this.set('viewDensity', viewDensity);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
actions: {
|
||||
onSetSort(val) {
|
||||
switch (val) {
|
||||
case 'name':
|
||||
this.set('sortBy.name', true);
|
||||
this.set('sortBy.created', false);
|
||||
this.set('sortBy.updated', false);
|
||||
break;
|
||||
case 'created':
|
||||
this.set('sortBy.name', false);
|
||||
this.set('sortBy.created', true);
|
||||
this.set('sortBy.updated', false);
|
||||
break;
|
||||
case 'updated':
|
||||
this.set('sortBy.name', false);
|
||||
this.set('sortBy.created', false);
|
||||
this.set('sortBy.updated', true);
|
||||
break;
|
||||
case 'asc':
|
||||
this.set('sortBy.asc', true);
|
||||
this.set('sortBy.desc', false);
|
||||
break;
|
||||
case 'desc':
|
||||
this.set('sortBy.asc', false);
|
||||
this.set('sortBy.desc', true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onSortBy(attacher) {
|
||||
// attacher.hide();
|
||||
this.get('onFiltered')(this.get('documents'));
|
||||
},
|
||||
|
||||
onSwitchView(v) {
|
||||
this.set('viewDensity', v);
|
||||
this.get('localStorage').storeSessionItem('space.density', v);
|
||||
},
|
||||
|
||||
onShowDeleteDocuments() {
|
||||
this.set('showDeleteDialog', true);
|
||||
},
|
||||
|
|
|
@ -28,7 +28,6 @@ export default Component.extend(AuthMixin, {
|
|||
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
|
||||
}),
|
||||
selectedFilter: '',
|
||||
spaceLabel: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
@ -56,7 +55,6 @@ export default Component.extend(AuthMixin, {
|
|||
|
||||
this.set('categories', categories);
|
||||
this.set('categoryLinkName', categories.length > 0 ? 'Manage' : 'Add');
|
||||
this.set('spaceLabel', _.find(this.get('labels'), {id: this.get('space.labelId')}));
|
||||
|
||||
schedule('afterRender', () => {
|
||||
if (this.get('categoryFilter') !== '') {
|
||||
|
@ -120,6 +118,16 @@ export default Component.extend(AuthMixin, {
|
|||
filtered = this.get('documentsLive');
|
||||
this.set('categoryFilter', '');
|
||||
break;
|
||||
|
||||
case 'add':
|
||||
filtered = this.get('recentAdd');
|
||||
this.set('categoryFilter', '');
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
filtered = this.get('recentUpdate');
|
||||
this.set('categoryFilter', '');
|
||||
break;
|
||||
}
|
||||
|
||||
categories.forEach((cat)=> {
|
||||
|
|
|
@ -14,12 +14,4 @@ import Component from '@ember/component';
|
|||
export default Component.extend({
|
||||
tagName: 'div',
|
||||
classNames: ['master-container'],
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,13 +10,23 @@
|
|||
// https://documize.com
|
||||
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
localStorage: service('localStorage'),
|
||||
resultPhrase: '',
|
||||
searchQuery: computed('keywords', function() {
|
||||
return encodeURIComponent(this.get('keywords'));
|
||||
}),
|
||||
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
|
||||
sortBy: {
|
||||
name: true,
|
||||
created: false,
|
||||
updated: false,
|
||||
asc: true,
|
||||
desc: false,
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
@ -26,7 +36,7 @@ export default Component.extend({
|
|||
let phrase = 'Nothing found';
|
||||
|
||||
if (docs.length > 0) {
|
||||
duped = _.uniq(docs, function (item) {
|
||||
duped = _.uniqBy(docs, function(item) {
|
||||
return item.get('documentId');
|
||||
});
|
||||
|
||||
|
@ -34,10 +44,84 @@ export default Component.extend({
|
|||
let docLabel = duped.length === 1 ? "document" : "documents";
|
||||
let i = docs.length;
|
||||
let j = duped.length;
|
||||
phrase = `${i} ${references} across ${j} ${docLabel}`;
|
||||
phrase = `${i} ${references} in ${j} ${docLabel}`;
|
||||
}
|
||||
|
||||
this.set('resultPhrase', phrase);
|
||||
this.set('documents', duped);
|
||||
|
||||
let sortBy = this.get('localStorage').getSessionItem('search.sortBy');
|
||||
if (!_.isNull(sortBy) && !_.isUndefined(sortBy)) {
|
||||
this.send('onSetSort', sortBy);
|
||||
}
|
||||
|
||||
let sortOrder = this.get('localStorage').getSessionItem('search.sortOrder');
|
||||
if (!_.isNull(sortOrder) && !_.isUndefined(sortOrder)) {
|
||||
this.send('onSetSort', sortOrder);
|
||||
}
|
||||
|
||||
this.sortResults(duped);
|
||||
},
|
||||
|
||||
sortResults(docs) {
|
||||
let ls = this.get('localStorage');
|
||||
let sortBy = this.get('sortBy');
|
||||
|
||||
if (_.isNull(docs)) return;
|
||||
|
||||
if (sortBy.name) {
|
||||
docs = docs.sortBy('document');
|
||||
ls.storeSessionItem('search.sortBy', 'name');
|
||||
}
|
||||
if (sortBy.created) {
|
||||
docs = docs.sortBy('created');
|
||||
ls.storeSessionItem('search.sortBy', 'created');
|
||||
}
|
||||
if (sortBy.updated) {
|
||||
docs = docs.sortBy('revised');
|
||||
ls.storeSessionItem('search.sortBy', 'updated');
|
||||
}
|
||||
if (sortBy.desc) {
|
||||
docs = docs.reverseObjects();
|
||||
ls.storeSessionItem('search.sortOrder', 'desc');
|
||||
} else {
|
||||
ls.storeSessionItem('search.sortOrder', 'asc');
|
||||
}
|
||||
|
||||
this.set('documents', docs);
|
||||
},
|
||||
|
||||
actions: {
|
||||
onSetSort(val) {
|
||||
switch (val) {
|
||||
case 'name':
|
||||
this.set('sortBy.name', true);
|
||||
this.set('sortBy.created', false);
|
||||
this.set('sortBy.updated', false);
|
||||
break;
|
||||
case 'created':
|
||||
this.set('sortBy.name', false);
|
||||
this.set('sortBy.created', true);
|
||||
this.set('sortBy.updated', false);
|
||||
break;
|
||||
case 'updated':
|
||||
this.set('sortBy.name', false);
|
||||
this.set('sortBy.created', false);
|
||||
this.set('sortBy.updated', true);
|
||||
break;
|
||||
case 'asc':
|
||||
this.set('sortBy.asc', true);
|
||||
this.set('sortBy.desc', false);
|
||||
break;
|
||||
case 'desc':
|
||||
this.set('sortBy.asc', false);
|
||||
this.set('sortBy.desc', true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onSortBy(attacher) {
|
||||
this.sortResults(this.get('documents'));
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
@ -20,10 +20,6 @@ export default Component.extend({
|
|||
keywords: '' ,
|
||||
matchFilter: null,
|
||||
|
||||
// init() {
|
||||
// this._super(...arguments);
|
||||
// },
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
this.set('keywords', this.get('filter'));
|
||||
|
|
|
@ -9,8 +9,27 @@
|
|||
//
|
||||
// https://documize.com
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import AuthMixin from '../../mixins/auth';
|
||||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend(AuthMixin, {
|
||||
localStorage: service(),
|
||||
viewDensity: "1",
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
let viewDensity = this.get('localStorage').getSessionItem('spaces.density');
|
||||
if (!_.isNull(viewDensity) && !_.isUndefined(viewDensity)) {
|
||||
this.set('viewDensity', viewDensity);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
onSwitchView(v) {
|
||||
this.set('viewDensity', v);
|
||||
this.get('localStorage').storeSessionItem('spaces.density', v);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ export default Component.extend({
|
|||
icon: '',
|
||||
color: '',
|
||||
light: false,
|
||||
outline: false,
|
||||
themed: false,
|
||||
dismiss: false,
|
||||
truncate: false,
|
||||
|
@ -48,6 +49,10 @@ export default Component.extend({
|
|||
bc += '-light';
|
||||
}
|
||||
|
||||
if (this.outline) {
|
||||
bc += '-outline';
|
||||
}
|
||||
|
||||
if (!this.uppercase) {
|
||||
bc += ' text-case-normal';
|
||||
}
|
||||
|
|
|
@ -353,6 +353,7 @@ let constants = EmberObject.extend({
|
|||
Send: 'Send',
|
||||
Share: 'Share',
|
||||
SignIn: 'Sign In',
|
||||
Sort: 'Sort',
|
||||
Unassigned: 'Unassigned',
|
||||
Update: 'Update',
|
||||
Upload: 'Upload',
|
||||
|
|
|
@ -67,4 +67,15 @@ export default Model.extend({
|
|||
|
||||
return '';
|
||||
}),
|
||||
|
||||
addRecent: computed('created', function() {
|
||||
let after = moment().subtract(7, 'days');
|
||||
return moment(this.get('created')).isSameOrAfter(after);
|
||||
}),
|
||||
|
||||
updateRecent: computed('created', function() {
|
||||
let after = moment().subtract(7, 'days');
|
||||
return moment(this.get('revised')).isSameOrAfter(after) &&
|
||||
moment(this.get('created')).isBefore(after);
|
||||
})
|
||||
});
|
||||
|
|
|
@ -23,6 +23,14 @@ export default Controller.extend(NotifierMixin, {
|
|||
queryParams: ['category'],
|
||||
category: '',
|
||||
filteredDocs: null,
|
||||
// eslint-disable-next-line ember/avoid-leaking-state-in-ember-objects
|
||||
sortBy: {
|
||||
name: true,
|
||||
created: false,
|
||||
updated: false,
|
||||
asc: true,
|
||||
desc: false,
|
||||
},
|
||||
|
||||
actions: {
|
||||
onRefresh() {
|
||||
|
@ -80,6 +88,30 @@ export default Controller.extend(NotifierMixin, {
|
|||
},
|
||||
|
||||
onFiltered(docs) {
|
||||
let ls = this.get('localStorage');
|
||||
let sortBy = this.get('sortBy');
|
||||
|
||||
if (_.isNull(docs)) return;
|
||||
|
||||
if (sortBy.name) {
|
||||
docs = docs.sortBy('name');
|
||||
ls.storeSessionItem('space.sortBy', 'name');
|
||||
}
|
||||
if (sortBy.created) {
|
||||
docs = docs.sortBy('created');
|
||||
ls.storeSessionItem('space.sortBy', 'created');
|
||||
}
|
||||
if (sortBy.updated) {
|
||||
docs = docs.sortBy('revised');
|
||||
ls.storeSessionItem('space.sortBy', 'updated');
|
||||
}
|
||||
if (sortBy.desc) {
|
||||
docs = docs.reverseObjects();
|
||||
ls.storeSessionItem('space.sortOrder', 'desc');
|
||||
} else {
|
||||
ls.storeSessionItem('space.sortOrder', 'asc');
|
||||
}
|
||||
|
||||
this.set('filteredDocs', docs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
|
||||
import { Promise as EmberPromise, hash } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend(AuthenticatedRouteMixin, {
|
||||
categoryService: service('category'),
|
||||
|
@ -37,22 +37,25 @@ export default Route.extend(AuthenticatedRouteMixin, {
|
|||
let folders = this.modelFor('folder').folders;
|
||||
folders.forEach(f => {
|
||||
f.set('selected', false);
|
||||
})
|
||||
});
|
||||
|
||||
let documents = this.modelFor('folder').documents;
|
||||
documents.forEach(d => {
|
||||
d.set('selected', false);
|
||||
})
|
||||
});
|
||||
|
||||
return hash({
|
||||
folder: this.modelFor('folder').folder,
|
||||
permissions: this.modelFor('folder').permissions,
|
||||
label: _.find(this.modelFor('folder').labels, {id: this.modelFor('folder').folder.get('labelId')}),
|
||||
labels: this.modelFor('folder').labels,
|
||||
folders: folders,
|
||||
documents: documents,
|
||||
documentsDraft: _.filter(documents, function(d) { return d.get('lifecycle') === constants.Lifecycle.Draft; }),
|
||||
documentsLive: _.filter(documents, function(d) { return d.get('lifecycle') === constants.Lifecycle.Live; }),
|
||||
templates: this.modelFor('folder').templates,
|
||||
recentAdd: _.filter(documents, function(d) { return d.get('addRecent'); }),
|
||||
recentUpdate: _.filter(documents, function(d) { return d.get('updateRecent'); }),
|
||||
showStartDocument: false,
|
||||
rootDocCount: 0,
|
||||
categories: this.get('categories'),
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
documents=model.documents
|
||||
documentsDraft=model.documentsDraft
|
||||
documentsLive=model.documentsLive
|
||||
recentAdd=model.recentAdd
|
||||
recentUpdate=model.recentUpdate
|
||||
categories=model.categories
|
||||
categorySummary=model.categorySummary
|
||||
categoryMembers=model.categoryMembers
|
||||
|
@ -20,6 +22,11 @@
|
|||
{{#layout/master-content}}
|
||||
<div class="grid-container-6-4">
|
||||
<div class="grid-cell-1">
|
||||
{{#if (eq model.folder.labelId "")}}
|
||||
<div class="space-label">Unclassified</div>
|
||||
{{else}}
|
||||
<div class="space-label" style={{{model.label.bgColor}}}>{{model.label.name}}</div>
|
||||
{{/if}}
|
||||
{{layout/logo-heading
|
||||
title=model.folder.name
|
||||
desc=model.folder.desc
|
||||
|
@ -37,13 +44,15 @@
|
|||
onRefresh=(action "onRefresh")}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{folder/documents-list
|
||||
documents=filteredDocs
|
||||
spaces=model.folders
|
||||
space=model.folder
|
||||
templates=model.templates
|
||||
permissions=model.permissions
|
||||
sortBy=sortBy
|
||||
onFiltered=(action "onFiltered")
|
||||
onExportDocument=(action "onExportDocument")
|
||||
onDeleteDocument=(action "onDeleteDocument")
|
||||
onMoveDocument=(action "onMoveDocument")}}
|
||||
|
|
|
@ -155,6 +155,7 @@ $color-white-dark-1: #f5f5f5;
|
|||
|
||||
// Documents, spaces card background color
|
||||
$color-card: #F6F4EE;
|
||||
$color-sidebar: #f2f8fd;
|
||||
|
||||
/**************************************************************
|
||||
* Theme colors.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// CSS GRID WITH FIXED SIDEBAR OUTSIDE GRID
|
||||
// CSS GRID LAYOUT
|
||||
|
||||
// Mobile-first layout
|
||||
.master-container {
|
||||
|
@ -124,7 +124,8 @@
|
|||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
z-index: 888;
|
||||
background-color: map-get($gray-shades, 100);
|
||||
// background-color: map-get($gray-shades, 100);
|
||||
background-color: $color-sidebar;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,11 +30,6 @@ body {
|
|||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
// list-style: none;
|
||||
// list-style-type: none;
|
||||
}
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
|
|
|
@ -266,7 +266,8 @@
|
|||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
word-break: normal;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
@import "ui-button";
|
||||
@import "ui-toolbar";
|
||||
@import "ui-icon-picker";
|
||||
@import "ui-option";
|
|
@ -4,6 +4,9 @@
|
|||
@mixin button-shadow() {
|
||||
box-shadow: 1px 1px 3px 0px map-get($gray-shades, 500);
|
||||
}
|
||||
@mixin button-shadow-none() {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
%dmz-button {
|
||||
display: inline-block;
|
||||
|
@ -153,6 +156,26 @@
|
|||
background-color: map-get($gray-shades, 300);
|
||||
}
|
||||
}
|
||||
.dmz-button-gray-outline {
|
||||
@extend %dmz-button;
|
||||
background-color: transparent;
|
||||
color: map-get($gray-shades, 700);
|
||||
border: 1px solid map-get($gray-shades, 300);
|
||||
@extend .no-select;
|
||||
outline: none;
|
||||
@include button-shadow-none();
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
@include button-shadow-none();
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: map-get($gray-shades, 800);
|
||||
border-color: map-get($gray-shades, 500);
|
||||
}
|
||||
}
|
||||
|
||||
.dmz-button-theme {
|
||||
@extend %dmz-button;
|
||||
|
|
47
gui/app/styles/core/ui/ui-option.scss
Normal file
47
gui/app/styles/core/ui/ui-option.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
.ui-option-picker {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0;
|
||||
|
||||
> .option {
|
||||
margin: 0;
|
||||
padding: 5px 11px;
|
||||
color: map-get($gray-shades, 600);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
|
||||
|
||||
&:hover {
|
||||
> .text {
|
||||
color: map-get($yellow-shades, 800);
|
||||
}
|
||||
}
|
||||
|
||||
> .text {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.00375rem;
|
||||
}
|
||||
}
|
||||
|
||||
> .selected {
|
||||
> .text {
|
||||
color: map-get($yellow-shades, 700) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-option-picker-horiz {
|
||||
> .option {
|
||||
display: inline-block;
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,12 @@
|
|||
.ember-attacher-popper {
|
||||
background-color: $color-white;
|
||||
font-size: 1rem;
|
||||
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.16), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
@include border-radius(3px);
|
||||
-webkit-box-shadow: 3px 3px 33px 0 rgba(0, 0, 0, 0.16), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 3px 3px 33px 0 rgba(0, 0, 0, 0.16), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
// -webkit-box-shadow: 0 20px 66px 0 rgba(34,48,73,0.2);
|
||||
// box-shadow: 0 20px 66px 0 rgba(34,48,73,0.2);
|
||||
|
||||
@include border-radius(5px);
|
||||
|
||||
> p {
|
||||
margin: 4px;
|
||||
|
@ -31,7 +35,7 @@
|
|||
|
||||
&:hover {
|
||||
color: $color-black;
|
||||
background-color: map-get($gray-shades, 100);
|
||||
background-color: map-get($yellow-shades, 100);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,6 +88,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
> .closer {
|
||||
color: map-get($gray-shades, 500);
|
||||
font-weight: 300;
|
||||
font-size: 1.7rem;
|
||||
cursor: pointer;
|
||||
padding: 5px 5px 0 0;
|
||||
display: block;
|
||||
text-align: right;
|
||||
|
||||
&:hover {
|
||||
color: map-get($gray-shades, 700);
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
> .form {
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
|
|
|
@ -25,14 +25,14 @@
|
|||
> .index-list {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.9rem;
|
||||
font-size: 1rem;
|
||||
overflow-x: hidden;
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 0;
|
||||
|
||||
.item {
|
||||
@extend .no-select;
|
||||
padding: 4px 0;
|
||||
padding: 5px 0;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
white-space: nowrap;
|
||||
|
@ -40,7 +40,7 @@
|
|||
|
||||
> .link {
|
||||
color: map-get($gray-shades, 800);
|
||||
font-weight: 400;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: map-get($yellow-shades, 600);
|
||||
|
@ -51,6 +51,7 @@
|
|||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.wysiwyg {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: $color-black-light-1;
|
||||
|
||||
table {
|
||||
|
@ -31,20 +31,31 @@
|
|||
ul {
|
||||
margin: 15px 0;
|
||||
padding: 0 0 0 40px;
|
||||
line-height: 20px;
|
||||
// line-height: 20px;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
|
||||
ol {
|
||||
li {
|
||||
// list-style-type: decimal;
|
||||
line-height: 20px;
|
||||
// line-height: 20px;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
// list-style-type: disc;
|
||||
line-height: 20px;
|
||||
// line-height: 20px;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
|
||||
> code {
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,6 +94,7 @@
|
|||
@include border-radius(3px);
|
||||
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||
color: map-get($gray-shades, 800);
|
||||
margin: 1.5rem 0;
|
||||
|
||||
> code {
|
||||
background-color: transparent;
|
||||
|
@ -95,6 +107,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
background-color: map-get($gray-shades, 100);
|
||||
border-left: 7px solid map-get($gray-shades, 200);
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
.space-label {
|
||||
@include border-radius(3px);
|
||||
@extend .no-select;
|
||||
display: inline-block;
|
||||
margin: 10px 0 13px 0;
|
||||
padding: 0.3rem 0.7rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 400;
|
||||
background-color: map-get($gray-shades, 600);
|
||||
color: map-get($gray-shades, 100);
|
||||
}
|
||||
|
||||
.view-space {
|
||||
> .documents {
|
||||
margin: 0;
|
||||
|
@ -5,6 +17,7 @@
|
|||
|
||||
> .document {
|
||||
@include card();
|
||||
box-shadow: none;
|
||||
list-style-type: none;
|
||||
margin: 0 0 2rem 0;
|
||||
padding: 15px 20px;
|
||||
|
|
|
@ -1,22 +1,90 @@
|
|||
<div class="view-space">
|
||||
|
||||
<div class="text-right">
|
||||
{{#ui/ui-toolbar dark=false light=false raised=false large=false bordered=false}}
|
||||
{{ui/ui-toolbar-icon icon=constants.Icon.Blocks color=constants.Color.Gray tooltip="Complete"
|
||||
selected=(eq viewDensity "1") onClick=(action "onSwitchView" "1")}}
|
||||
{{ui/ui-toolbar-icon icon=constants.Icon.All color=constants.Color.Gray tooltip="Comfort"
|
||||
selected=(eq viewDensity "2") onClick=(action "onSwitchView" "2")}}
|
||||
{{ui/ui-toolbar-label label="—" color=constants.Color.Gray tooltip="Compact"
|
||||
selected=(eq viewDensity "3") onClick=(action "onSwitchView" "3")}}
|
||||
{{/ui/ui-toolbar}}
|
||||
|
||||
{{#ui/ui-button
|
||||
light=false
|
||||
outline=true
|
||||
uppercase=false
|
||||
color=constants.Color.Gray
|
||||
label=constants.Label.Sort}}
|
||||
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="click" showOn="click" isShown=false placement="bottom-end" as |attacher|}}
|
||||
<i class="dicon {{constants.Icon.Cross}} closer" {{action attacher.hide}}/>
|
||||
<div class="container">
|
||||
{{ui/ui-spacer size=100}}
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="ui-option-picker ui-option-picker-horiz">
|
||||
<li class="option {{if sortBy.name "selected"}}" {{action "onSetSort" "name"}}>
|
||||
<div class="text">Name</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.created "selected"}}" {{action "onSetSort" "created"}}>
|
||||
<div class="text">Created date</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.updated "selected"}}" {{action "onSetSort" "updated"}}>
|
||||
<div class="text">Last updated</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=100}}
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="ui-option-picker ui-option-picker-horiz">
|
||||
<li class="option {{if sortBy.asc "selected"}}" {{action "onSetSort" "asc"}}>
|
||||
<div class="text">Ascending</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.desc "selected"}}" {{action "onSetSort" "desc"}}>
|
||||
<div class="text">Descending</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=300}}
|
||||
|
||||
{{ui/ui-button
|
||||
light=true
|
||||
color=constants.Color.Yellow
|
||||
label=constants.Label.Sort
|
||||
onClick=(action "onSortBy" attacher)}}
|
||||
</div>
|
||||
{{/attach-popover}}
|
||||
{{/ui/ui-button}}
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=200}}
|
||||
|
||||
<ul class="documents">
|
||||
{{#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="desc">{{ document.excerpt }}</div>
|
||||
{{#if (not-eq viewDensity "3")}}
|
||||
<div class="desc">{{ document.excerpt }}</div>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
<div class="meta">
|
||||
<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"}}">
|
||||
{{document.lifecycleLabel}}
|
||||
{{#if (eq viewDensity "1")}}
|
||||
<div class="meta">
|
||||
<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"}}">
|
||||
{{document.lifecycleLabel}}
|
||||
</div>
|
||||
</div>
|
||||
{{folder/document-categories categories=document.category}}
|
||||
{{folder/document-tags documentTags=document.tags}}
|
||||
</div>
|
||||
{{folder/document-categories categories=document.category}}
|
||||
{{folder/document-tags documentTags=document.tags}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if hasDocumentActions}}
|
||||
<div class="checkbox" {{action "selectDocument" document.id}}>
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
{{ui/ui-spacer size=300}}
|
||||
|
||||
<div class="title">label</div>
|
||||
{{#if (eq space.labelId "")}}
|
||||
<div class="label">Unclassified</div>
|
||||
{{else}}
|
||||
<div class="label" style={{{spaceLabel.bgColor}}}>{{spaceLabel.name}}</div>
|
||||
{{/if}}
|
||||
|
||||
{{ui/ui-spacer size=200}}
|
||||
|
||||
<div class="title">filter</div>
|
||||
<div class="list">
|
||||
<div class="item {{if (eq selectedFilter "space") "selected"}}" {{action "onDocumentFilter" "space" space.id}}>
|
||||
|
@ -39,6 +30,14 @@
|
|||
<div class="name">Live ({{documentsLive.length}})</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="item {{if (eq selectedFilter "add") "selected"}}" {{action "onDocumentFilter" "add" space.id}}>
|
||||
<i class={{concat "dicon " constants.Icon.Filter}} />
|
||||
<div class="name">Added recently ({{recentAdd.length}})</div>
|
||||
</div>
|
||||
<div class="item {{if (eq selectedFilter "update") "selected"}}" {{action "onDocumentFilter" "update" space.id}}>
|
||||
<i class={{concat "dicon " constants.Icon.Filter}} />
|
||||
<div class="name">Updated recently ({{recentUpdate.length}})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=200}}
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
{{layout/page-desc desc=desc}}
|
||||
</div>
|
||||
</div>
|
||||
{{ui/ui-spacer size=300}}
|
||||
{{ui/ui-spacer size=200}}
|
|
@ -50,7 +50,7 @@
|
|||
{{#if hasPins}}
|
||||
<div class="bookmarks" id="user-pins-button">
|
||||
<i class={{concat "dicon " constants.Icon.BookmarkSolid}}></i>
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="clickout" showOn="click" isShown=false}}
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="clickout click" showOn="click" isShown=false}}
|
||||
<div class="menu">
|
||||
{{#if hasSpacePins}}
|
||||
<li class="item header">Spaces</li>
|
||||
|
@ -79,7 +79,7 @@
|
|||
<div class="update-available-dot" />
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="clickout" showOn="click" isShown=false}}
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="clickout click" showOn="click" isShown=false}}
|
||||
<div class="menu">
|
||||
{{#link-to "profile" class="item"}}Profile{{/link-to}}
|
||||
{{#if session.isAdmin}}
|
||||
|
|
|
@ -1,4 +1,61 @@
|
|||
<div class="view-search">
|
||||
|
||||
{{#if documents}}
|
||||
<div class="text-right">
|
||||
{{#ui/ui-button
|
||||
light=false
|
||||
outline=true
|
||||
uppercase=false
|
||||
color=constants.Color.Gray
|
||||
label=constants.Label.Sort}}
|
||||
|
||||
{{#attach-popover class="ember-attacher-popper" hideOn="click" showOn="click" isShown=false placement="bottom-end" as |attacher|}}
|
||||
<i class="dicon {{constants.Icon.Cross}} closer" {{action attacher.hide}}/>
|
||||
<div class="container">
|
||||
{{ui/ui-spacer size=100}}
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="ui-option-picker ui-option-picker-horiz">
|
||||
<li class="option {{if sortBy.name "selected"}}" {{action "onSetSort" "name"}}>
|
||||
<div class="text">Name</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.created "selected"}}" {{action "onSetSort" "created"}}>
|
||||
<div class="text">Created date</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.updated "selected"}}" {{action "onSetSort" "updated"}}>
|
||||
<div class="text">Last updated</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=100}}
|
||||
|
||||
<div class="text-center">
|
||||
<ul class="ui-option-picker ui-option-picker-horiz">
|
||||
<li class="option {{if sortBy.asc "selected"}}" {{action "onSetSort" "asc"}}>
|
||||
<div class="text">Ascending</div>
|
||||
</li>
|
||||
<li class="option {{if sortBy.desc "selected"}}" {{action "onSetSort" "desc"}}>
|
||||
<div class="text">Descending</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=300}}
|
||||
|
||||
{{ui/ui-button
|
||||
light=true
|
||||
color=constants.Color.Yellow
|
||||
label=constants.Label.Sort
|
||||
onClick=(action "onSortBy" attacher)}}
|
||||
</div>
|
||||
{{/attach-popover}}
|
||||
{{/ui/ui-button}}
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=200}}
|
||||
{{/if}}
|
||||
|
||||
<div class="result-summary">{{resultPhrase}}</div>
|
||||
<ul class="documents">
|
||||
{{#each documents key="id" as |result|}}
|
||||
|
@ -17,4 +74,5 @@
|
|||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
</div>
|
|
@ -1,4 +1,18 @@
|
|||
<div class="view-spaces">
|
||||
|
||||
<div class="text-right">
|
||||
{{#ui/ui-toolbar dark=false light=false raised=false large=false bordered=false}}
|
||||
{{ui/ui-toolbar-icon icon=constants.Icon.Blocks color=constants.Color.Gray tooltip="Complete"
|
||||
selected=(eq viewDensity "1") onClick=(action "onSwitchView" "1")}}
|
||||
{{ui/ui-toolbar-icon icon=constants.Icon.All color=constants.Color.Gray tooltip="Comfort"
|
||||
selected=(eq viewDensity "2") onClick=(action "onSwitchView" "2")}}
|
||||
{{ui/ui-toolbar-label label="—" color=constants.Color.Gray tooltip="Compact"
|
||||
selected=(eq viewDensity "3") onClick=(action "onSwitchView" "3")}}
|
||||
{{/ui/ui-toolbar}}
|
||||
</div>
|
||||
|
||||
{{ui/ui-spacer size=200}}
|
||||
|
||||
<ul class="list">
|
||||
{{#each spaces as |space|}}
|
||||
{{#link-to "folder.index" space.id space.slug}}
|
||||
|
@ -12,40 +26,47 @@
|
|||
{{/if}}
|
||||
{{space.name}}
|
||||
</div>
|
||||
<div class="desc">{{space.desc}} </div>
|
||||
<div class="meta">
|
||||
{{!-- {{#if (eq space.spaceType constants.SpaceType.Public)}}
|
||||
<i class={{concat "dicon " constants.Icon.World}}>
|
||||
{{#attach-tooltip showDelay=1000}}Public space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}}
|
||||
{{#if (eq space.spaceType constants.SpaceType.Protected)}}
|
||||
<i class={{concat "dicon " constants.Icon.People}}>
|
||||
{{#attach-tooltip showDelay=1000}}Protected space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}}
|
||||
{{#if (eq space.spaceType constants.SpaceType.Private)}}
|
||||
<i class={{concat "dicon " constants.Icon.Person}}>
|
||||
{{#attach-tooltip showDelay=1000}}Personal space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}} --}}
|
||||
{{#if space.labelId}}
|
||||
{{spaces/space-label labels=labels labelId=space.labelId}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if (not-eq viewDensity "3")}}
|
||||
<div class="desc">{{space.desc}} </div>
|
||||
{{/if}}
|
||||
{{#if (eq viewDensity "1")}}
|
||||
<div class="meta">
|
||||
{{!-- {{#if (eq space.spaceType constants.SpaceType.Public)}}
|
||||
<i class={{concat "dicon " constants.Icon.World}}>
|
||||
{{#attach-tooltip showDelay=1000}}Public space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}}
|
||||
{{#if (eq space.spaceType constants.SpaceType.Protected)}}
|
||||
<i class={{concat "dicon " constants.Icon.People}}>
|
||||
{{#attach-tooltip showDelay=1000}}Protected space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}}
|
||||
{{#if (eq space.spaceType constants.SpaceType.Private)}}
|
||||
<i class={{concat "dicon " constants.Icon.Person}}>
|
||||
{{#attach-tooltip showDelay=1000}}Personal space{{/attach-tooltip}}
|
||||
</i>
|
||||
{{/if}} --}}
|
||||
{{#if space.labelId}}
|
||||
{{spaces/space-label labels=labels labelId=space.labelId}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="number">{{space.countContent}}</div>
|
||||
<div class="label">items</div>
|
||||
{{#if (eq viewDensity "1")}}
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="number">{{space.countContent}}</div>
|
||||
<div class="label">items</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="number">{{space.countCategory}}</div>
|
||||
<div class="label">categories</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="number">{{space.countCategory}}</div>
|
||||
<div class="label">categories</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/link-to}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{{#if tooltip}}
|
||||
{{#attach-tooltip showDelay=1000}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{#attach-tooltip showDelay=750}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{/if}}
|
||||
{{yield}}
|
|
@ -1,4 +1,4 @@
|
|||
{{label}}
|
||||
{{#if tooltip}}
|
||||
{{#attach-tooltip showDelay=1000}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{#attach-tooltip showDelay=750}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{/if}}
|
|
@ -1,4 +1,4 @@
|
|||
{{yield}}
|
||||
{{#if tooltip}}
|
||||
{{#attach-tooltip showDelay=1000}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{#attach-tooltip showDelay=750}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{/if}}
|
|
@ -1,3 +1,3 @@
|
|||
{{#if tooltip}}
|
||||
{{#attach-tooltip showDelay=1000}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{#attach-tooltip showDelay=750}}{{tooltip}}{{/attach-tooltip}}
|
||||
{{/if}}
|
|
@ -28,7 +28,11 @@ module.exports = function (environment) {
|
|||
|
||||
// Ember Attacher: tooltips & popover component defaults
|
||||
emberAttacher: {
|
||||
arrow: false
|
||||
arrow: false,
|
||||
animation: 'fill',
|
||||
// popperOptions: {
|
||||
// placement: 'bottom right'
|
||||
// }
|
||||
},
|
||||
|
||||
"ember-cli-mirage": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "documize",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.0",
|
||||
"description": "The Document IDE",
|
||||
"repository": "",
|
||||
"license": "AGPL",
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
|
||||
package search
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// QueryOptions defines how we search.
|
||||
type QueryOptions struct {
|
||||
Keywords string `json:"keywords"`
|
||||
|
@ -23,18 +27,20 @@ type QueryOptions struct {
|
|||
|
||||
// QueryResult represents 'presentable' search results.
|
||||
type QueryResult struct {
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
ItemID string `json:"itemId"`
|
||||
ItemType string `json:"itemType"`
|
||||
DocumentID string `json:"documentId"`
|
||||
DocumentSlug string `json:"documentSlug"`
|
||||
Document string `json:"document"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Tags string `json:"tags"`
|
||||
SpaceID string `json:"spaceId"`
|
||||
Space string `json:"space"`
|
||||
SpaceSlug string `json:"spaceSlug"`
|
||||
Template bool `json:"template"`
|
||||
VersionID string `json:"versionId"`
|
||||
ID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
ItemID string `json:"itemId"`
|
||||
ItemType string `json:"itemType"`
|
||||
DocumentID string `json:"documentId"`
|
||||
DocumentSlug string `json:"documentSlug"`
|
||||
Document string `json:"document"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Tags string `json:"tags"`
|
||||
SpaceID string `json:"spaceId"`
|
||||
Space string `json:"space"`
|
||||
SpaceSlug string `json:"spaceSlug"`
|
||||
Template bool `json:"template"`
|
||||
VersionID string `json:"versionId"`
|
||||
Created time.Time `json:"created"`
|
||||
Revised time.Time `json:"revised"`
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue