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

Enable custom logo upload and rendering

This commit is contained in:
McMatts 2019-01-06 13:50:12 +00:00
parent a211ba051a
commit 036f36ba1d
36 changed files with 574 additions and 211 deletions

View file

@ -46,7 +46,6 @@ import (
"github.com/documize/community/model/doc"
"github.com/documize/community/model/group"
"github.com/documize/community/model/link"
"github.com/documize/community/model/org"
"github.com/documize/community/model/page"
"github.com/documize/community/model/permission"
"github.com/documize/community/model/pin"
@ -231,14 +230,14 @@ func (b backerHandler) dmzOrg(files *[]backupItem) (err error) {
w = fmt.Sprintf(" WHERE c_refid='%s' ", b.Spec.OrgID)
}
o := []org.Organization{}
o := []orgExtended{}
err = b.Runtime.Db.Select(&o, `SELECT id, c_refid AS refid,
c_title AS title, c_message AS message, c_domain AS domain,
c_service AS conversionendpoint, c_email AS email, c_serial AS serial, c_active AS active,
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_sub,`+b.Runtime.StoreProvider.JSONEmpty()+`) AS subscription,
coalesce(c_authconfig,`+b.Runtime.StoreProvider.JSONEmpty()+`) AS authconfig, c_maxtags AS maxtags,
c_created AS created, c_revised AS revised
c_theme AS theme, c_logo AS logo, c_created AS created, c_revised AS revised
FROM dmz_org`+w)
if err != nil {
return
@ -255,10 +254,6 @@ func (b backerHandler) dmzOrg(files *[]backupItem) (err error) {
// Config, User Config.
func (b backerHandler) dmzConfig(files *[]backupItem) (err error) {
type config struct {
ConfigKey string `json:"key"`
ConfigValue string `json:"config"`
}
c := []config{}
err = b.Runtime.Db.Select(&c, `SELECT c_key AS configkey, c_config AS configvalue FROM dmz_config`)
if err != nil {
@ -279,14 +274,7 @@ func (b backerHandler) dmzConfig(files *[]backupItem) (err error) {
w = fmt.Sprintf(" where c_orgid='%s' ", b.Spec.OrgID)
}
type userConfig struct {
OrgID string `json:"orgId"`
UserID string `json:"userId"`
ConfigKey string `json:"key"`
ConfigValue string `json:"config"`
}
uc := []userConfig{}
err = b.Runtime.Db.Select(&uc, `select c_orgid AS orgid, c_userid AS userid,
c_key AS configkey, c_config AS configvalue FROM dmz_user_config`+w)
if err != nil {
@ -475,6 +463,8 @@ func (b backerHandler) dmzSpace(files *[]backupItem) (err error) {
err = b.Runtime.Db.Select(&sp, `SELECT id, c_refid AS refid,
c_name AS name, c_orgid AS orgid, c_userid AS userid,
c_type AS type, c_lifecycle AS lifecycle, c_likes AS likes,
c_icon AS icon, c_labelid AS labelid, c_desc AS description,
c_count_category As countcategory, c_count_content AS countcontent,
c_created AS created, c_revised AS revised
FROM dmz_space`+w)
if err != nil {
@ -671,16 +661,6 @@ func (b backerHandler) dmzDocument(files *[]backupItem) (err error) {
*files = append(*files, backupItem{Filename: "dmz_doc.json", Content: content})
// Vote
type vote struct {
RefID string `json:"refId"`
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
VoterID string `json:"voterId"`
Vote int `json:"vote"`
Created time.Time `json:"created"`
Revised time.Time `json:"revised"`
}
vt := []vote{}
err = b.Runtime.Db.Select(&vt, `
SELECT c_refid AS refid, c_orgid AS orgid,
@ -716,16 +696,6 @@ func (b backerHandler) dmzDocument(files *[]backupItem) (err error) {
*files = append(*files, backupItem{Filename: "dmz_doc_link.json", Content: content})
// Comment
type comment struct {
RefID string `json:"feedbackId"`
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
UserID string `json:"userId"`
Email string `json:"email"`
Feedback string `json:"feedback"`
Created time.Time `json:"created"`
}
cm := []comment{}
err = b.Runtime.Db.Select(&cm, `
SELECT c_refid AS refid, c_orgid AS orgid, c_docid AS documentid,
@ -743,20 +713,6 @@ func (b backerHandler) dmzDocument(files *[]backupItem) (err error) {
*files = append(*files, backupItem{Filename: "dmz_doc_comment.json", Content: content})
// Share
type share struct {
ID uint64 `json:"id"`
OrgID string `json:"orgId"`
UserID string `json:"userId"`
DocumentID string `json:"documentId"`
Email string `json:"email"`
Message string `json:"message"`
Viewed string `json:"viewed"` // recording each view as |date-viewed|date-viewed|
Secret string `json:"secret"` // secure token used to access document
Expires string `json:"expires"` // number of days from creation, value of 0 means never
Active bool `json:"active"`
Created time.Time `json:"created"`
}
sh := []share{}
err = b.Runtime.Db.Select(&sh, `
SELECT id AS id, c_orgid AS orgid, c_docid AS documentid,

78
domain/backup/models.go Normal file
View file

@ -0,0 +1,78 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package backup handle data backup/restore to/from ZIP format.
package backup
// Existing data models do not necessarily have fields to hold
// all data when loaded from the database.
// So we extend the existing models to hold additional fields
// for a complete backup and restore process.
import (
"time"
"github.com/documize/community/model/org"
)
type orgExtended struct {
org.Organization
Logo []byte `json:"logo"`
}
type config struct {
ConfigKey string `json:"key"`
ConfigValue string `json:"config"`
}
type userConfig struct {
OrgID string `json:"orgId"`
UserID string `json:"userId"`
ConfigKey string `json:"key"`
ConfigValue string `json:"config"`
}
// Vote
type vote struct {
RefID string `json:"refId"`
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
VoterID string `json:"voterId"`
Vote int `json:"vote"`
Created time.Time `json:"created"`
Revised time.Time `json:"revised"`
}
// Comment
type comment struct {
RefID string `json:"feedbackId"`
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
UserID string `json:"userId"`
Email string `json:"email"`
Feedback string `json:"feedback"`
Created time.Time `json:"created"`
}
// Share
type share struct {
ID uint64 `json:"id"`
OrgID string `json:"orgId"`
UserID string `json:"userId"`
DocumentID string `json:"documentId"`
Email string `json:"email"`
Message string `json:"message"`
Viewed string `json:"viewed"` // recording each view as |date-viewed|date-viewed|
Secret string `json:"secret"` // secure token used to access document
Expires string `json:"expires"` // number of days from creation, value of 0 means never
Active bool `json:"active"`
Created time.Time `json:"created"`
}

View file

@ -39,7 +39,6 @@ import (
"github.com/documize/community/model/doc"
"github.com/documize/community/model/group"
"github.com/documize/community/model/link"
"github.com/documize/community/model/org"
"github.com/documize/community/model/page"
"github.com/documize/community/model/permission"
"github.com/documize/community/model/pin"
@ -332,7 +331,7 @@ func (r *restoreHandler) readZip(filename string) (found bool, b []byte, err err
func (r *restoreHandler) dmzOrg() (err error) {
filename := "dmz_org.json"
org := []org.Organization{}
org := []orgExtended{}
err = r.fileJSON(filename, &org)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to load %s", filename))
@ -363,12 +362,14 @@ func (r *restoreHandler) dmzOrg() (err error) {
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind(`
INSERT INTO dmz_org (c_refid, c_company, c_title, c_message,
c_domain, c_service, c_email, c_anonaccess, c_authprovider, c_authconfig,
c_maxtags, c_verified, c_serial, c_sub, c_active, c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
c_maxtags, c_verified, c_serial, c_sub, c_active,
c_theme, c_logo, c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
org[i].RefID, org[i].Company, org[i].Title, org[i].Message,
strings.ToLower(org[i].Domain), org[i].ConversionEndpoint, strings.ToLower(org[i].Email),
org[i].AllowAnonymousAccess, org[i].AuthProvider, org[i].AuthConfig,
org[i].MaxTags, true, org[i].Serial, org[i].Subscription,
org[i].Theme, org[i].Logo,
org[i].Active, org[i].Created, org[i].Revised)
if err != nil {
r.Context.Transaction.Rollback()
@ -402,6 +403,7 @@ func (r *restoreHandler) dmzOrg() (err error) {
org[0].Serial = r.Spec.Org.Serial
org[0].Title = r.Spec.Org.Title
org[0].Subscription = r.Spec.Org.Subscription
org[0].Theme = r.Spec.Org.Theme
}
_, err = r.Context.Transaction.NamedExec(`UPDATE dmz_org SET
@ -612,8 +614,16 @@ func (r *restoreHandler) dmzSpace() (err error) {
}
for i := range sp {
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind("INSERT INTO dmz_space (c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle, c_likes, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
sp[i].RefID, sp[i].Name, r.remapOrg(sp[i].OrgID), r.remapUser(sp[i].UserID), sp[i].Type, sp[i].Lifecycle, sp[i].Likes, sp[i].Created, sp[i].Revised)
_, err = r.Context.Transaction.Exec(r.Runtime.Db.Rebind(`
INSERT INTO dmz_space
(c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle,
c_likes, c_icon, c_desc, c_count_category, c_count_content,
c_labelid, c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
sp[i].RefID, sp[i].Name, r.remapOrg(sp[i].OrgID),
r.remapUser(sp[i].UserID), sp[i].Type, sp[i].Lifecycle,
sp[i].Likes, sp[i].Icon, sp[i].Description, sp[i].CountCategory,
sp[i].CountContent, sp[i].LabelID, sp[i].Created, sp[i].Revised)
if err != nil {
r.Context.Transaction.Rollback()

View file

@ -13,6 +13,7 @@ package meta
import (
"bytes"
"encoding/base64"
"fmt"
"net/http"
"strings"
@ -301,3 +302,41 @@ func (h *Handler) Themes(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, th)
}
// Logo returns site logo based upon request domain (e.g. acme.documize.com).
// The default Documize logo is returned if organization has not uploaded
// their own logo.
func (h *Handler) Logo(w http.ResponseWriter, r *http.Request) {
ctx := domain.GetRequestContext(r)
d := organization.GetSubdomainFromHost(r)
logo, err := h.Store.Organization.Logo(ctx, d)
if err != nil {
h.Runtime.Log.Infof("unable to fetch logo for domain %s", d)
response.WriteNotFound(w)
return
}
if len(string(logo)) == 0 {
logo, err = base64.StdEncoding.DecodeString(defaultLogo)
if err != nil {
h.Runtime.Log.Error("unable to decode default logo", err)
response.WriteEmpty(w)
return
}
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(logo)))
w.WriteHeader(http.StatusOK)
_, err = w.Write(logo)
if err != nil {
h.Runtime.Log.Error("failed to write org logo", err)
return
}
}
var defaultLogo = `
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
`

View file

@ -12,8 +12,11 @@
package organization
import (
"bytes"
"database/sql"
"encoding/json"
"github.com/documize/community/model/audit"
"io"
"io/ioutil"
"net/http"
@ -99,3 +102,50 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, org)
}
// UploadLogo stores log for organization.
func (h *Handler) UploadLogo(w http.ResponseWriter, r *http.Request) {
method := "organization.UploadLogo"
ctx := domain.GetRequestContext(r)
if !ctx.Administrator {
response.WriteForbiddenError(w)
return
}
// We use default logo if body is empty.
logo := []byte{}
filedata, _, err := r.FormFile("attachment")
if err == nil {
b := new(bytes.Buffer)
_, err = io.Copy(b, filedata)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
} else {
logo = b.Bytes()
}
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Organization.UploadLogo(ctx, logo)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeOrganizationLogo)
response.WriteEmpty(w)
}

View file

@ -52,7 +52,7 @@ func (s Store) GetOrganization(ctx domain.RequestContext, id string) (org org.Or
c_anonaccess AS allowanonymousaccess, c_authprovider AS authprovider,
coalesce(c_authconfig,`+s.EmptyJSON()+`) AS authconfig,
coalesce(c_sub,`+s.EmptyJSON()+`) AS subscription,
c_maxtags AS maxtags, c_created AS created, c_revised AS revised, c_theme AS theme
c_maxtags AS maxtags, c_theme AS theme, c_created AS created, c_revised AS revised
FROM dmz_org
WHERE c_refid=?`),
id)
@ -176,3 +176,31 @@ func (s Store) CheckDomain(ctx domain.RequestContext, domain string) string {
return ""
}
// Logo fetchs stored image from store or NULL.
func (s Store) Logo(ctx domain.RequestContext, domain string) (l []byte, err error) {
row := s.Runtime.Db.QueryRow(s.Bind("SELECT c_logo FROM dmz_org WHERE c_domain=? AND c_active=true"), domain)
err = row.Scan(&l)
if err == sql.ErrNoRows {
err = nil
return nil, nil
}
if err != nil {
return nil, err
}
return l, nil
}
// UploadLogo saves custom logo to the organization record.
func (s Store) UploadLogo(ctx domain.RequestContext, logo []byte) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("UPDATE dmz_org SET c_logo=?, c_revised=? WHERE c_refid=?"),
logo, time.Now().UTC(), ctx.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to save custom logo for org %s", ctx.OrgID))
}
return
}

View file

@ -99,6 +99,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
var sp space.Space
sp.Name = model.Name
sp.Description = model.Description
sp.Icon = model.Icon
sp.LabelID = model.LabelID
sp.RefID = uniqueid.Generate()
sp.OrgID = ctx.OrgID
sp.UserID = ctx.UserID
@ -675,52 +678,52 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
ok := true
ctx.Transaction, ok = h.Runtime.StartTx()
if !ok {
if !ok {
response.WriteError(w, method)
return
}
_, err := h.Store.Document.DeleteBySpace(ctx, id)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
_, err = h.Store.Permission.DeleteSpacePermissions(ctx, id)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
// remove category permissions
_, err = h.Store.Permission.DeleteSpaceCategoryPermissions(ctx, id)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
_, err = h.Store.Pin.DeletePinnedSpace(ctx, id)
if err != nil && err != sql.ErrNoRows {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
// remove category and members for space
_, err = h.Store.Category.DeleteBySpace(ctx, id)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
_, err = h.Store.Space.Delete(ctx, id)
if err != nil {
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
h.Runtime.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
return
}
@ -728,22 +731,22 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
h.Runtime.Commit(ctx.Transaction)
// Record this action.
ctx.Transaction, ok = h.Runtime.StartTx()
if !ok {
response.WriteError(w, method)
return
}
ctx.Transaction, ok = h.Runtime.StartTx()
if !ok {
response.WriteError(w, method)
return
}
err = 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.Rollback(ctx.Transaction)
response.WriteServerError(w, method, err)
}
h.Runtime.Commit(ctx.Transaction)
h.Runtime.Commit(ctx.Transaction)
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete)

View file

@ -30,7 +30,12 @@ type Store struct {
// Add adds new folder into the store.
func (s Store) Add(ctx domain.RequestContext, sp space.Space) (err error) {
_, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_space (c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle, c_likes, c_icon, c_desc, c_count_category, c_count_content, c_labelid, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"),
_, err = ctx.Transaction.Exec(s.Bind(`
INSERT INTO dmz_space
(c_refid, c_name, c_orgid, c_userid, c_type, c_lifecycle,
c_likes, c_icon, c_desc, c_count_category, c_count_content,
c_labelid, c_created, c_revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes,
sp.Icon, sp.Description, sp.CountCategory, sp.CountContent, sp.LabelID,
sp.Created, sp.Revised)

View file

@ -154,6 +154,8 @@ type OrganizationStorer interface {
RemoveOrganization(ctx domain.RequestContext, orgID string) (err error)
UpdateAuthConfig(ctx domain.RequestContext, org org.Organization) (err error)
CheckDomain(ctx domain.RequestContext, domain string) string
Logo(ctx domain.RequestContext, domain string) (l []byte, err error)
UploadLogo(ctx domain.RequestContext, l []byte) (err error)
}
// PinStorer defines required methods for pin management

View file

@ -32,6 +32,51 @@ export default Component.extend(Notifier, {
this.set('maxTags', this.get('model.general.maxTags'));
},
didInsertElement() {
this._super(...arguments);
let self = this;
let url = this.get('appMeta.endpoint');
let orgId = this.get('appMeta.orgId');
let uploadUrl = `${url}/organization/${orgId}/logo`;
let dzone = new Dropzone("#upload-logo > div", {
headers: {
'Authorization': 'Bearer ' + self.get('session.authToken')
},
url: uploadUrl,
method: "post",
paramName: 'attachment',
clickable: true,
maxFilesize: 50,
parallelUploads: 1,
uploadMultiple: false,
addRemoveLinks: false,
autoProcessQueue: true,
createImageThumbnails: false,
init: function () {
this.on("success", function (/*file, response*/ ) {
});
this.on("queuecomplete", function () {
self.notifySuccess('Logo uploaded');
});
this.on("error", function (error, msg) {
self.notifyError(msg);
self.notifyError(error);
});
}
});
dzone.on("complete", function (file) {
dzone.removeFile(file);
});
this.set('drop', dzone);
},
actions: {
change() {
const selectEl = this.$('#maxTags')[0];
@ -63,7 +108,7 @@ export default Component.extend(Notifier, {
this.set('model.general.maxTags', this.get('maxTags'));
this.get('save')().then(() => {
this.get('onUpdate')().then(() => {
this.notifySuccess('Saved');
set(this, 'titleError', false);
set(this, 'messageError', false);
@ -74,6 +119,11 @@ export default Component.extend(Notifier, {
onThemeChange(theme) {
this.get('appMeta').setTheme(theme);
this.set('model.general.theme', theme);
},
onDefaultLogo() {
this.get('onDefaultLogo')(this.get('appMeta.orgId'));
this.notifySuccess('Using default logo');
}
}
});

View file

@ -20,6 +20,7 @@ import Component from '@ember/component';
export default Component.extend(AuthMixin, Notifier, {
router: service(),
spaceSvc: service('folder'),
iconSvc: service('icon'),
localStorage: service('localStorage'),
isSpaceAdmin: computed('permissions', function() {
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
@ -40,7 +41,7 @@ export default Component.extend(AuthMixin, Notifier, {
init() {
this._super(...arguments);
this.populateIconList();
this.set('iconList', this.get('iconSvc').getSpaceIconList());
},
didReceiveAttrs() {
@ -76,62 +77,6 @@ export default Component.extend(AuthMixin, Notifier, {
this.set('spaceIcon', icon);
},
populateIconList() {
let list = this.get('iconList');
let constants = this.get('constants');
list = A([]);
list.pushObject(constants.IconMeta.Star);
list.pushObject(constants.IconMeta.Support);
list.pushObject(constants.IconMeta.Message);
list.pushObject(constants.IconMeta.Apps);
list.pushObject(constants.IconMeta.Box);
list.pushObject(constants.IconMeta.Gift);
list.pushObject(constants.IconMeta.Design);
list.pushObject(constants.IconMeta.Bulb);
list.pushObject(constants.IconMeta.Metrics);
list.pushObject(constants.IconMeta.PieChart);
list.pushObject(constants.IconMeta.BarChart);
list.pushObject(constants.IconMeta.Finance);
list.pushObject(constants.IconMeta.Lab);
list.pushObject(constants.IconMeta.Code);
list.pushObject(constants.IconMeta.Help);
list.pushObject(constants.IconMeta.Manuals);
list.pushObject(constants.IconMeta.Flow);
list.pushObject(constants.IconMeta.Out);
list.pushObject(constants.IconMeta.In);
list.pushObject(constants.IconMeta.Partner);
list.pushObject(constants.IconMeta.Org);
list.pushObject(constants.IconMeta.Home);
list.pushObject(constants.IconMeta.Infinite);
list.pushObject(constants.IconMeta.Todo);
list.pushObject(constants.IconMeta.Procedure);
list.pushObject(constants.IconMeta.Outgoing);
list.pushObject(constants.IconMeta.Incoming);
list.pushObject(constants.IconMeta.Travel);
list.pushObject(constants.IconMeta.Winner);
list.pushObject(constants.IconMeta.Roadmap);
list.pushObject(constants.IconMeta.Money);
list.pushObject(constants.IconMeta.Security);
list.pushObject(constants.IconMeta.Tune);
list.pushObject(constants.IconMeta.Guide);
list.pushObject(constants.IconMeta.Smile);
list.pushObject(constants.IconMeta.Rocket);
list.pushObject(constants.IconMeta.Time);
list.pushObject(constants.IconMeta.Cup);
list.pushObject(constants.IconMeta.Marketing);
list.pushObject(constants.IconMeta.Announce);
list.pushObject(constants.IconMeta.Devops);
list.pushObject(constants.IconMeta.World);
list.pushObject(constants.IconMeta.Plan);
list.pushObject(constants.IconMeta.Components);
list.pushObject(constants.IconMeta.People);
list.pushObject(constants.IconMeta.Checklist);
this.set('iconList', list);
},
actions: {
onSetSpaceType(t) {
this.set('spaceType', t);

View file

@ -9,9 +9,20 @@
//
// https://documize.com
import { inject as service } from '@ember/service';
import Component from '@ember/component';
export default Component.extend({
appMeta: service(),
icon: null,
meta: null
meta: null,
logo: false,
didReceiveAttrs() {
this._super(...arguments);
if (this.get('logo')) {
let cb = + new Date();
this.set('cacheBuster', cb);
}
}
});

View file

@ -0,0 +1,19 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import stringUtil from '../utils/string';
import { helper } from '@ember/component/helper';
// Usage: {{random-id}}
export default helper(function() {
return stringUtil.makeId(10);
});

View file

@ -1,8 +1,9 @@
<div class="auth-center">
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.appHost}}</div>
<img src="{{appMeta.endpoint}}/public/logo?cb={{random-id}}"
title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.title}} ({{appMeta.appHost}})</div>
</div>
<div class="login-form">
{{user/forgot-password forgot=(action "forgot")}}

View file

@ -2,8 +2,9 @@
{{#if model.showLogin}}
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.appHost}}</div>
<img src="{{appMeta.endpoint}}/public/logo?cb={{random-id}}"
title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.title}} ({{appMeta.appHost}})</div>
</div>
<form {{action "login" on="submit"}}>
<div class="form-group">

View file

@ -1,8 +1,9 @@
<div class="auth-center">
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.appHost}}</div>
<img src="{{appMeta.endpoint}}/public/logo?cb={{random-id}}"
title="Documize" alt="Documize" class="img-fluid">
<div class="url">{{appMeta.title}} ({{appMeta.appHost}})</div>
</div>
{{user/password-reset reset=(action "reset")}}
</div>

View file

@ -16,9 +16,13 @@ export default Controller.extend({
orgService: service('organization'),
actions: {
save() {
onUpdate() {
return this.get('orgService').save(this.model.general).then(() => {
});
},
onDefaultLogo(orgId) {
return this.get('orgService').useDefaultLogo(orgId);
}
}
});

View file

@ -3,4 +3,6 @@
desc="Options to help you customize Documize"
icon=constants.Icon.Settings}}
{{customize/general-settings model=model save=(action "save")}}
{{customize/general-settings model=model
onUpdate=(action "onUpdate")
onDefaultLogo=(action "onDefaultLogo")}}

View file

@ -20,10 +20,10 @@
{{#layout/master-content}}
<div class="grid-container-6-4">
<div class="grid-cell-1">
{{layout/logo-heading
title=model.folder.name
desc=model.folder.desc
meta=model.folder.icon}}
{{layout/logo-heading
title=model.folder.name
desc=model.folder.desc
meta=model.folder.icon}}
</div>
<div class="grid-cell-2 grid-cell-right">
{{folder/space-toolbar

View file

@ -19,19 +19,20 @@ import Controller from '@ember/controller';
export default Controller.extend(AuthMixin, Modals, {
appMeta: service(),
folderService: service('folder'),
spaceName: '',
copyTemplate: true,
copyPermission: true,
copyDocument: false,
hasClone: notEmpty('clonedSpace.id'),
clonedSpace: null,
selectedView: 'all',
selectedSpaces: null,
publicSpaces: null,
protectedSpaces: null,
personalSpaces: null,
spaceIcon: '',
spaceLabel: '',
spaceDesc: '',
spaceName: '',
actions: {
onShowModal() {
@ -42,10 +43,22 @@ export default Controller.extend(AuthMixin, Modals, {
this.set('clonedSpace', sp)
},
onSetIcon(icon) {
this.set('spaceIcon', icon);
},
onSetLabel(id) {
this.set('spaceLabel', id);
},
onAddSpace(e) {
e.preventDefault();
let spaceName = this.get('spaceName');
let spaceDesc = this.get('spaceDesc');
let spaceIcon = this.get('spaceIcon');
let spaceLabel = this.get('spaceLabel');
let clonedId = this.get('clonedSpace.id');
if (is.empty(spaceName)) {
@ -55,6 +68,9 @@ export default Controller.extend(AuthMixin, Modals, {
let payload = {
name: spaceName,
desc: spaceDesc,
icon: spaceIcon,
labelId: spaceLabel,
cloneId: clonedId,
copyTemplate: this.get('copyTemplate'),
copyPermission: this.get('copyPermission'),
@ -62,6 +78,9 @@ export default Controller.extend(AuthMixin, Modals, {
}
this.set('spaceName', '');
this.set('spaceDesc', '');
this.set('spaceIcon', '');
this.set('spaceLabel', '');
this.set('clonedSpace', null);
$("#new-space-name").removeClass("is-invalid");

View file

@ -17,6 +17,7 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Route.extend(AuthenticatedRouteMixin, {
appMeta: service(),
folderService: service('folder'),
iconSvc: service('icon'),
localStorage: service(),
labelSvc: service('label'),
@ -66,6 +67,7 @@ export default Route.extend(AuthenticatedRouteMixin, {
controller.set('publicSpaces', publicSpaces);
controller.set('protectedSpaces', protectedSpaces);
controller.set('personalSpaces', personalSpaces);
controller.set('iconList', this.get('iconSvc').getSpaceIconList());
},
activate() {

View file

@ -32,11 +32,13 @@
{{#if labels}}
<div class="list">
{{#each labels as |label|}}
<div class="item {{if (eq selectedView label.id) "selected"}}" {{action "onSelect" label.id}}>
<i class={{concat "dicon " constants.Icon.Checkbox}}
style={{label.bgfgColor}}/>
<div class="name">{{label.name}} ({{label.count}})</div>
</div>
{{#if (gt label.count 0)}}
<div class="item {{if (eq selectedView label.id) "selected"}}" {{action "onSelect" label.id}}>
<i class={{concat "dicon " constants.Icon.Checkbox}}
style={{label.bgfgColor}}/>
<div class="name">{{label.name}} ({{label.count}})</div>
</div>
{{/if}}
{{/each}}
</div>
{{else}}
@ -48,8 +50,10 @@
{{#layout/master-content}}
<div class="grid-container-8-2">
<div class="grid-cell-1">
{{layout/page-heading title=appMeta.title}}
{{layout/page-desc desc=appMeta.message}}
{{layout/logo-heading
title=appMeta.title
desc=appMeta.message
logo=true}}
</div>
<div class="grid-cell-2 grid-cell-right">
{{#if (or session.isEditor session.isAdmin)}}
@ -68,16 +72,49 @@
{{spaces/space-list spaces=selectedSpaces labels=labels}}
<div class="modal" tabindex="-1" role="dialog" id="add-space-modal">
<div class="modal-dialog" role="document">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">Add Space</div>
<div class="modal-header">New Space</div>
<div class="modal-body">
<form onsubmit={{action "onAddSpace"}}>
<div class="form-group">
<label for="new-space-name">Space Name</label>
<label for="new-space-name">Name</label>
{{input type="text" id="new-space-name" class="form-control mousetrap" placeholder="Space name" value=spaceName}}
<small class="form-text text-muted">Characters and numbers only</small>
</div>
<div class="form-group">
<label>Description</label>
{{focus-input id="space-desc" type="text" value=spaceDesc class="form-control" placeholder="Space description" autocomplete="off"}}
</div>
<div class="form-group">
<label>Icon</label>
<div class="ui-icon-picker">
<ul class="list">
{{#each iconList as |icon|}}
<li class="item {{if (eq spaceIcon icon) "selected"}}" {{action "onSetIcon" icon}}>
{{ui/ui-icon-meta icon=icon}}
</li>
{{/each}}
</ul>
</div>
</div>
<div class="form-group">
<label>Label</label>
<ul class="space-label-picker">
<li class="label none {{if (eq spaceLabel "") "selected"}}" {{action "onSetLabel" ""}}>None</li>
{{#each labels as |label|}}
<li class="label {{if (eq spaceLabel label.id) "selected"}}"
style={{label.bgColor}}
{{action "onSetLabel" label.id}} title={{label.name}}>
{{label.name}}
</li>
{{/each}}
</ul>
</div>
<div class="form-group">
<label for="clone-space-dropdown">Clone Space</label>
{{ui/ui-select id="clone-space-dropdown" content=model prompt="select space" action=(action "onCloneSpaceSelect") optionValuePath="id" optionLabelPath="name" selection=clonedSpace}}

69
gui/app/services/icon.js Normal file
View file

@ -0,0 +1,69 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import { A } from '@ember/array';
import Service from '@ember/service';
export default Service.extend({
getSpaceIconList() {
let cs = this.get('constants');
let list = A([]);
list.pushObject(cs.IconMeta.Star);
list.pushObject(cs.IconMeta.Support);
list.pushObject(cs.IconMeta.Message);
list.pushObject(cs.IconMeta.Apps);
list.pushObject(cs.IconMeta.Box);
list.pushObject(cs.IconMeta.Gift);
list.pushObject(cs.IconMeta.Design);
list.pushObject(cs.IconMeta.Bulb);
list.pushObject(cs.IconMeta.Metrics);
list.pushObject(cs.IconMeta.PieChart);
list.pushObject(cs.IconMeta.BarChart);
list.pushObject(cs.IconMeta.Finance);
list.pushObject(cs.IconMeta.Lab);
list.pushObject(cs.IconMeta.Code);
list.pushObject(cs.IconMeta.Help);
list.pushObject(cs.IconMeta.Manuals);
list.pushObject(cs.IconMeta.Flow);
list.pushObject(cs.IconMeta.Out);
list.pushObject(cs.IconMeta.In);
list.pushObject(cs.IconMeta.Partner);
list.pushObject(cs.IconMeta.Org);
list.pushObject(cs.IconMeta.Home);
list.pushObject(cs.IconMeta.Infinite);
list.pushObject(cs.IconMeta.Todo);
list.pushObject(cs.IconMeta.Procedure);
list.pushObject(cs.IconMeta.Outgoing);
list.pushObject(cs.IconMeta.Incoming);
list.pushObject(cs.IconMeta.Travel);
list.pushObject(cs.IconMeta.Winner);
list.pushObject(cs.IconMeta.Roadmap);
list.pushObject(cs.IconMeta.Money);
list.pushObject(cs.IconMeta.Security);
list.pushObject(cs.IconMeta.Tune);
list.pushObject(cs.IconMeta.Guide);
list.pushObject(cs.IconMeta.Smile);
list.pushObject(cs.IconMeta.Rocket);
list.pushObject(cs.IconMeta.Time);
list.pushObject(cs.IconMeta.Cup);
list.pushObject(cs.IconMeta.Marketing);
list.pushObject(cs.IconMeta.Announce);
list.pushObject(cs.IconMeta.Devops);
list.pushObject(cs.IconMeta.World);
list.pushObject(cs.IconMeta.Plan);
list.pushObject(cs.IconMeta.Components);
list.pushObject(cs.IconMeta.People);
list.pushObject(cs.IconMeta.Checklist);
return list;
}
});

View file

@ -72,5 +72,12 @@ export default Service.extend({
method: 'POST',
data: JSON.stringify(config)
});
},
useDefaultLogo(orgId) {
return this.get('ajax').request(`organization/${orgId}/logo`, {
method: 'POST',
data: '',
});
}
});

View file

@ -15,7 +15,7 @@
display: flex;
flex-direction: row;
> .icon, > .meta-icon {
> .icon, > .meta-icon, > .meta-logo {
align-self: center;
margin-right: 25px;

View file

@ -339,4 +339,10 @@
color: $color-white;
}
}
> #upload-logo {
.dz-preview, .dz-processing, .dz-file-preview {
display: none !important;
}
}
}

View file

@ -254,7 +254,7 @@
@extend .text-truncate;
display: inline-block;
width: 200px;
margin: 0 20px 20px 0;
margin: 0 10px 10px 0;
padding: 0.5rem 0.75rem;
font-size: 1rem;
font-weight: 500;

View file

@ -22,13 +22,13 @@
justify-self: self-start;
> .name {
font-size: 1.3rem;
font-size: 1.4rem;
font-weight: 700;
color: map-get($gray-shades, 800);
> .icon {
color: map-get($gray-shades, 700);
font-size: 20px;
font-size: 26px;
vertical-align: middle;
display: inline-block;
margin-right: 10px;

View file

@ -1,47 +1,55 @@
<div class="view-customize">
<form>
<div class="form-group">
<label for="siteTitle">Site Name</label>
{{focus-input id="siteTitle" type="text" value=model.general.title class=(if hasTitleInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">Provide short title for this Documize instance</small>
</div>
<div class="form-group">
<label id="siteMessage">Site Message</label>
{{textarea id="siteMessage" rows="3" value=model.general.message class=(if hasMessageInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">Provide short message explaining this Documize instance</small>
</div>
<div class="form-group">
<label>Site Theme</label>
{{ui/theme-picker onChange=(action "onThemeChange")}}
<small class="form-text text-muted">Users can set their own theme under Profile</small>
</div>
<div class="form-group">
<label>Public Spaces Viewable By Anonymous Users</label>
{{x-toggle value=model.general.allowAnonymousAccess size="medium" theme="light" onToggle=(action (mut model.general.allowAnonymousAccess))}}
</div>
<div class="form-group">
<label for="conversionEndpoint">Conversion Service URL</label>
{{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">
Endpoint for handling import/export (e.g. https://api.documize.com,
<a href="https://docs.documize.com/s/WNEpptWJ9AABRnha/administration-guides/d/WO0pt_MXigAB6sJ7/general-options">view documentation</a>)
</small>
</div>
<div class="form-group">
<label for="maxTags">Maximum Tags Per Document</label>
<select class="form-control" id="maxTags" {{action "change" on="change"}}>
<option selected={{is-equal 3 maxTags}} value="3">3</option>
<option selected={{is-equal 4 maxTags}} value="4">4</option>
<option selected={{is-equal 5 maxTags}} value="5">5</option>
<option selected={{is-equal 6 maxTags}} value="6">6</option>
<option selected={{is-equal 7 maxTags}} value="7">7</option>
<option selected={{is-equal 8 maxTags}} value="8">8</option>
<option selected={{is-equal 9 maxTags}} value="9">9</option>
<option selected={{is-equal 10 maxTags}} value="10">10</option>
</select>
<small class="form-text text-muted">How many tags can be assigned to a document (between 3 and 10 tags)</small>
<div class="form-group">
<label for="siteTitle">Site Name</label>
{{focus-input id="siteTitle" type="text" value=model.general.title class=(if hasTitleInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">Provide short title for this Documize instance</small>
</div>
<div class="form-group">
<label id="siteMessage">Site Message</label>
{{textarea id="siteMessage" rows="3" value=model.general.message class=(if hasMessageInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">Provide short message explaining this Documize instance</small>
</div>
<div class="form-group">
<label>Site Theme</label>
{{ui/theme-picker onChange=(action "onThemeChange")}}
</div>
<div class="form-group">
<label>Site Logo</label>
<div>
{{ui/ui-button light=true color=constants.Color.Gray label="Use Default" onClick=(action "onDefaultLogo")}}
{{ui/ui-button-gap}}
{{ui/ui-button light=true color=constants.Color.Yellow label="Upload Custom" id="upload-logo"}}
</div>
<small class="form-text text-muted">You can choose to upload a small logo (e.g. 64px x 64px)</small>
</div>
{{ui/ui-button color=constants.Color.Green light=true icon=constants.Icon.Settings label=constants.Label.Save onClick=(action "save")}}
</form>
<div class="form-group">
<label>Public Spaces Viewable By Anonymous Users</label>
{{x-toggle value=model.general.allowAnonymousAccess size="medium" theme="light" onToggle=(action (mut model.general.allowAnonymousAccess))}}
<small class="form-text text-muted">Share content with unauthenticated site visitors</small>
</div>
<div class="form-group">
<label for="conversionEndpoint">Conversion Service URL</label>
{{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError "form-control is-invalid" "form-control")}}
<small class="form-text text-muted">
Endpoint for handling import/export (e.g. https://api.documize.com,
<a href="https://docs.documize.com/s/WNEpptWJ9AABRnha/administration-guides/d/WO0pt_MXigAB6sJ7/general-options">read the documentation</a>)
</small>
</div>
<div class="form-group">
<label for="maxTags">Maximum Tags Per Document</label>
<select class="form-control" id="maxTags" {{action "change" on="change"}}>
<option selected={{is-equal 3 maxTags}} value="3">3</option>
<option selected={{is-equal 4 maxTags}} value="4">4</option>
<option selected={{is-equal 5 maxTags}} value="5">5</option>
<option selected={{is-equal 6 maxTags}} value="6">6</option>
<option selected={{is-equal 7 maxTags}} value="7">7</option>
<option selected={{is-equal 8 maxTags}} value="8">8</option>
<option selected={{is-equal 9 maxTags}} value="9">9</option>
<option selected={{is-equal 10 maxTags}} value="10">10</option>
</select>
<small class="form-text text-muted">How many tags can be assigned to a document (between 3 and 10 tags)</small>
</div>
{{ui/ui-button color=constants.Color.Green light=true icon=constants.Icon.Settings label=constants.Label.Save onClick=(action "save")}}
</div>

View file

@ -10,7 +10,7 @@
<ul class="space-labels">
{{#each labels as |label|}}
<li class="label" style={{concat "background-color:" label.color ";"}}>
<li class="label" style={{label.bgColor}}>
<div class="grid-container-6-4">
<div class="grid-cell-1 grid-cell-middle">
{{label.name}}

View file

@ -38,7 +38,7 @@
<li class="label none {{if (eq spaceLabel "") "selected"}}" {{action "onSetLabel" ""}}>None</li>
{{#each labels as |label|}}
<li class="label {{if (eq spaceLabel label.id) "selected"}}"
style={{concat "background-color:" label.color ";"}}
style={{label.bgColor}}
{{action "onSetLabel" label.id}} title={{label.name}}>
{{label.name}}
</li>

View file

@ -7,6 +7,11 @@
<div class="meta-icon">
{{ui/ui-icon-meta icon=meta}}
</div>
{{else if logo}}
<div class="meta-logo">
<img src="{{appMeta.endpoint}}/public/logo?cb={{cacheBuster}}"
style="max-width: 200px; max-height: 100px;">
</div>
{{/if}}
<div class="text">
{{layout/page-heading title=title}}

View file

@ -99,6 +99,7 @@ const (
EventTypeLabelAdd EventType = "added-label"
EventTypeLabelUpdate EventType = "updated-label"
EventTypeLabelDelete EventType = "removed-label"
EventTypeOrganizationLogo EventType = "uploaded-logo"
// EventTypeVersionAdd records addition of version
EventTypeVersionAdd EventType = "added-version"

View file

@ -44,5 +44,4 @@ type SiteMeta struct {
Storage env.StoreType `json:"storageProvider"`
Location string `json:"location"` // reserved for internal use
Theme string `json:"theme"` // default side-wide theme, user overrideble
Logo []byte `json:"logo"`
}

View file

@ -94,6 +94,9 @@ type InvitationModel struct {
// NewSpaceRequest details the new space to create.
type NewSpaceRequest struct {
Name string `json:"name"`
Description string `json:"desc"`
LabelID string `json:"labelId"`
Icon string `json:"icon"`
CloneID string `json:"cloneId"` // existing space to clone, empty = no cloning
CopyTemplate bool `json:"copyTemplate"` // copy templates and reusable content blocks
CopyPermission bool `json:"copyPermission"` // copy uer permissions

View file

@ -95,6 +95,7 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
AddPublic(rt, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
AddPublic(rt, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
AddPublic(rt, "attachment/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
AddPublic(rt, "logo", []string{"GET", "OPTIONS"}, nil, meta.Logo)
//**************************************************
// Secured private routes (require authentication)
@ -131,6 +132,7 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) {
AddPrivate(rt, "organization/{orgID}", []string{"PUT", "OPTIONS"}, nil, organization.Update)
AddPrivate(rt, "organization/{orgID}/setting", []string{"GET", "OPTIONS"}, nil, setting.GetInstanceSetting)
AddPrivate(rt, "organization/{orgID}/setting", []string{"POST", "OPTIONS"}, nil, setting.SaveInstanceSetting)
AddPrivate(rt, "organization/{orgID}/logo", []string{"POST", "OPTIONS"}, nil, organization.UploadLogo)
AddPrivate(rt, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
AddPrivate(rt, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)