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

New config setting: how many tags per document?

This commit is contained in:
Harvey Kandola 2018-07-05 12:02:10 -04:00
parent 0743ae002c
commit 19736aab04
17 changed files with 827 additions and 731 deletions

View file

@ -58,9 +58,9 @@ Space view.
## Latest version ## Latest version
[Community edition: v1.66.0](https://github.com/documize/community/releases) [Community edition: v1.67.0](https://github.com/documize/community/releases)
[Enterprise edition: v1.68.0](https://documize.com/downloads) [Enterprise edition: v1.69.0](https://documize.com/downloads)
## OS support ## OS support

View file

@ -0,0 +1,6 @@
/* community edition */
-- max tags per document setting
ALTER TABLE organization ADD COLUMN `maxtags` INT NOT NULL DEFAULT 3 AFTER `authconfig`;
-- deprecations

View file

@ -54,6 +54,7 @@ func (h *Handler) Meta(w http.ResponseWriter, r *http.Request) {
data.AllowAnonymousAccess = org.AllowAnonymousAccess data.AllowAnonymousAccess = org.AllowAnonymousAccess
data.AuthProvider = org.AuthProvider data.AuthProvider = org.AuthProvider
data.AuthConfig = org.AuthConfig data.AuthConfig = org.AuthConfig
data.MaxTags = org.MaxTags
data.Version = h.Runtime.Product.Version data.Version = h.Runtime.Product.Version
data.Edition = h.Runtime.Product.License.Edition data.Edition = h.Runtime.Product.License.Edition
data.Valid = h.Runtime.Product.License.Valid data.Valid = h.Runtime.Product.License.Valid

View file

@ -36,9 +36,9 @@ func (s Scope) AddOrganization(ctx domain.RequestContext, org org.Organization)
org.Revised = time.Now().UTC() org.Revised = time.Now().UTC()
_, err = ctx.Transaction.Exec( _, err = ctx.Transaction.Exec(
"INSERT INTO organization (refid, company, title, message, url, domain, email, allowanonymousaccess, serial, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "INSERT INTO organization (refid, company, title, message, url, domain, email, allowanonymousaccess, serial, maxtags, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.URL), strings.ToLower(org.Domain), org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.URL), strings.ToLower(org.Domain),
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.Created, org.Revised) strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "unable to execute insert for org") err = errors.Wrap(err, "unable to execute insert for org")
@ -49,7 +49,7 @@ func (s Scope) AddOrganization(ctx domain.RequestContext, org org.Organization)
// GetOrganization returns the Organization reocrod from the organization database table with the given id. // GetOrganization returns the Organization reocrod from the organization database table with the given id.
func (s Scope) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) { func (s Scope) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE refid=?") stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE refid=?")
defer streamutil.Close(stmt) defer streamutil.Close(stmt)
if err != nil { if err != nil {
@ -80,14 +80,14 @@ func (s Scope) GetOrganizationByDomain(subdomain string) (o org.Organization, er
} }
// match on given domain name // match on given domain name
err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain=? AND active=1", subdomain) err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain=? AND active=1", subdomain)
if err == nil { if err == nil {
return return
} }
err = nil err = nil
// match on empty domain as last resort // match on empty domain as last resort
err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain='' AND active=1") err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain='' AND active=1")
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "unable to execute select for empty subdomain") err = errors.Wrap(err, "unable to execute select for empty subdomain")
} }
@ -99,7 +99,7 @@ func (s Scope) GetOrganizationByDomain(subdomain string) (o org.Organization, er
func (s Scope) UpdateOrganization(ctx domain.RequestContext, org org.Organization) (err error) { func (s Scope) UpdateOrganization(ctx domain.RequestContext, org org.Organization) (err error) {
org.Revised = time.Now().UTC() org.Revised = time.Now().UTC()
_, err = ctx.Transaction.NamedExec("UPDATE organization SET title=:title, message=:message, service=:conversionendpoint, email=:email, allowanonymousaccess=:allowanonymousaccess, revised=:revised WHERE refid=:refid", _, err = ctx.Transaction.NamedExec("UPDATE organization SET title=:title, message=:message, service=:conversionendpoint, email=:email, allowanonymousaccess=:allowanonymousaccess, maxtags=:maxtags, revised=:revised WHERE refid=:refid",
&org) &org)
if err != nil { if err != nil {

View file

@ -41,7 +41,7 @@ func main() {
// product details // product details
rt.Product = env.ProdInfo{} rt.Product = env.ProdInfo{}
rt.Product.Major = "1" rt.Product.Major = "1"
rt.Product.Minor = "66" rt.Product.Minor = "67"
rt.Product.Patch = "0" rt.Product.Patch = "0"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Community" rt.Product.Edition = "Community"

File diff suppressed because one or more lines are too long

View file

@ -17,6 +17,7 @@ import Notifier from '../../mixins/notifier';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend(Notifier, { export default Component.extend(Notifier, {
maxTags: 3,
titleEmpty: empty('model.general.title'), titleEmpty: empty('model.general.title'),
messageEmpty: empty('model.general.message'), messageEmpty: empty('model.general.message'),
conversionEndpointEmpty: empty('model.general.conversionEndpoint'), conversionEndpointEmpty: empty('model.general.conversionEndpoint'),
@ -24,7 +25,19 @@ export default Component.extend(Notifier, {
hasMessageInputError: and('messageEmpty', 'messageError'), hasMessageInputError: and('messageEmpty', 'messageError'),
hasConversionEndpointInputError: and('conversionEndpointEmpty', 'conversionEndpointError'), hasConversionEndpointInputError: and('conversionEndpointEmpty', 'conversionEndpointError'),
didReceiveAttrs() {
this._super(...arguments);
this.set('maxTags', this.get('model.general.maxTags'));
},
actions: { actions: {
change() {
const selectEl = this.$('#maxTags')[0];
const selection = selectEl.selectedOptions[0].value;
this.set('maxTags', parseInt(selection));
},
save() { save() {
if (isEmpty(this.get('model.general.title'))) { if (isEmpty(this.get('model.general.title'))) {
set(this, 'titleError', true); set(this, 'titleError', true);
@ -46,6 +59,7 @@ export default Component.extend(Notifier, {
this.set('model.general.conversionEndpoint', e.substring(0, e.length-1)); this.set('model.general.conversionEndpoint', e.substring(0, e.length-1));
} }
this.set('model.general.maxTags', this.get('maxTags'));
this.model.general.set('allowAnonymousAccess', $("#allowAnonymousAccess").prop('checked')); this.model.general.set('allowAnonymousAccess', $("#allowAnonymousAccess").prop('checked'));
this.showWait(); this.showWait();

View file

@ -18,9 +18,11 @@ import Notifier from '../../mixins/notifier';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend(Notifier, { export default Component.extend(Notifier, {
appMeta: service(),
documentSvc: service('document'), documentSvc: service('document'),
categoryService: service('category'), categoryService: service('category'),
tagz: A([]),
categories: A([]), categories: A([]),
newCategory: '', newCategory: '',
showCategoryModal: false, showCategoryModal: false,
@ -34,11 +36,6 @@ export default Component.extend(Notifier, {
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage'); return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
}), }),
maxTags: 3,
tag1: '',
tag2: '',
tag3: '',
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
this.load(); this.load();
@ -48,7 +45,7 @@ export default Component.extend(Notifier, {
this._super(...arguments); this._super(...arguments);
schedule('afterRender', () => { schedule('afterRender', () => {
$("#add-tag-field0").focus(); $("#add-tag-field-1").focus();
$(".tag-input").off("keydown").on("keydown", function(e) { $(".tag-input").off("keydown").on("keydown", function(e) {
if (e.shiftKey && e.which === 9) { if (e.shiftKey && e.which === 9) {
@ -59,7 +56,20 @@ export default Component.extend(Notifier, {
return false; return false;
} }
if (e.which === 9 || e.which === 13 || e.which === 16 || e.which === 45 || e.which === 189 || e.which === 8 || e.which === 127 || (e.which >= 65 && e.which <= 90) || (e.which >= 97 && e.which <= 122) || (e.which >= 48 && e.which <= 57)) { if (e.which === 9 ||
e.which === 13 ||
e.which === 16 ||
e.which === 37 ||
e.which === 38 ||
e.which === 39 ||
e.which === 40 ||
e.which === 45 ||
e.which === 189 ||
e.which === 8 ||
e.which === 127 ||
(e.which >= 65 && e.which <= 90) ||
(e.which >= 97 && e.which <= 122) ||
(e.which >= 48 && e.which <= 57)) {
return true; return true;
} }
@ -90,17 +100,27 @@ export default Component.extend(Notifier, {
}); });
}); });
let counter = 1;
let tagz = A([]);
let maxTags = this.get('appMeta.maxTags');
if (!_.isUndefined(this.get('document.tags')) && this.get('document.tags').length > 1) { if (!_.isUndefined(this.get('document.tags')) && this.get('document.tags').length > 1) {
let tags = this.get('document.tags').split('#'); let tags = this.get('document.tags').split('#');
let counter = 1;
_.each(tags, (tag) => { _.each(tags, (tag) => {
tag = tag.trim(); tag = tag.trim();
if (tag.length > 0) { if (tag.length > 0 && counter <= maxTags) {
this.set('tag' + counter, tag); tagz.pushObject({number: counter, value: tag});
counter++; counter++;
} }
}); });
} }
for (let index = counter; index <= maxTags; index++) {
tagz.pushObject({number: index, value: ''});
}
this.set('tagz', tagz);
}, },
actions: { actions: {
@ -142,27 +162,26 @@ export default Component.extend(Notifier, {
}); });
}); });
let tag1 = this.get("tag1").toLowerCase().trim(); let tagz = this.get('tagz');
let tag2 = this.get("tag2").toLowerCase().trim(); let tagzToSave = [];
let tag3 = this.get("tag3").toLowerCase().trim();
_.each(tagz, (t) => {
let tag = t.value.toLowerCase().trim();
if (tag.length> 0) {
if (!_.contains(tagzToSave, tag) && is.not.startWith(tag, '-')) {
tagzToSave.push(tag);
this.$('#add-tag-field-' + t.number).removeClass('is-invalid');
} else {
this.$('#add-tag-field-' + t.number).addClass('is-invalid');
}
}
});
let save = "#"; let save = "#";
_.each(tagzToSave, (t) => {
if (is.startWith(tag1, '-')) { save += t;
$('#add-tag-field1').addClass('is-invalid'); save += '#';
return; });
}
if (is.startWith(tag2, '-')) {
$('#add-tag-field2').addClass('is-invalid');
return;
}
if (is.startWith(tag3, '-')) {
$('#add-tag-field3').addClass('is-invalid');
return;
}
(tag1.length > 0 ) ? save += (tag1 + "#") : this.set('tag1', '');
(tag2.length > 0 && tag2 !== tag1) ? save += (tag2 + "#") : this.set('tag2', '');
(tag3.length > 0 && tag3 !== tag1 && tag3 !== tag2) ? save += (tag3 + "#") : this.set('tag3', '');
let doc = this.get('document'); let doc = this.get('document');
doc.set('tags', save); doc.set('tags', save);

View file

@ -11,7 +11,6 @@
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
// import { belongsTo, hasMany } from 'ember-data/relationships';
export default Model.extend({ export default Model.extend({
title: attr('string'), title: attr('string'),
@ -19,6 +18,7 @@ export default Model.extend({
email: attr('string'), email: attr('string'),
conversionEndpoint: attr('string'), conversionEndpoint: attr('string'),
allowAnonymousAccess: attr('boolean', { defaultValue: false }), allowAnonymousAccess: attr('boolean', { defaultValue: false }),
maxTags: attr('number', {defaultValue: 3}),
created: attr(), created: attr(),
revised: attr() revised: attr()
}); });

View file

@ -29,7 +29,7 @@ export default Service.extend({
message: '', message: '',
edition: 'Community', edition: 'Community',
// for major.minor semver release detection // for major.minor semver release detection
// for bugfix releases, only admin is made aware of new release and end users see no Whats New messaging // for bugfix releases, only admin is made aware of new release and end users see no What's New messaging
updateAvailable: false, updateAvailable: false,
valid: true, valid: true,
allowAnonymousAccess: false, allowAnonymousAccess: false,
@ -37,6 +37,7 @@ export default Service.extend({
authConfig: null, authConfig: null,
setupMode: false, setupMode: false,
secureMode: false, secureMode: false,
maxTags: 3,
invalidLicense() { invalidLicense() {
return this.valid === false; return this.valid === false;

View file

@ -34,6 +34,7 @@ export default Service.extend({
this.get('appMeta').setProperties({ this.get('appMeta').setProperties({
message: org.get('message'), message: org.get('message'),
title: org.get('title'), title: org.get('title'),
maxTags: org.get('maxTags'),
conversionEndpoint: org.get('conversionEndpoint') conversionEndpoint: org.get('conversionEndpoint')
}); });

View file

@ -10,22 +10,22 @@
<div class="view-customize"> <div class="view-customize">
<form class="mt-5"> <form class="mt-5">
<div class="form-group row"> <div class="form-group row">
<label for="siteTitle" class="col-sm-2 col-form-label">Instance Name</label> <label for="siteTitle" class="col-sm-4 col-form-label">Site Name</label>
<div class="col-sm-10"> <div class="col-sm-7">
{{focus-input id="siteTitle" type="text" value=model.general.title class=(if hasTitleInputError 'form-control is-invalid' 'form-control')}} {{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> <small class="form-text text-muted">Provide short title for this Documize instance</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label id="siteMessage" class="col-sm-2 col-form-label">Message</label> <label id="siteMessage" class="col-sm-4 col-form-label">Site Message</label>
<div class="col-sm-10"> <div class="col-sm-7">
{{textarea id="siteMessage" rows="3" value=model.general.message class=(if hasMessageInputError 'form-control is-invalid' 'form-control')}} {{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> <small class="form-text text-muted">Provide short message explaining this Documize instance</small>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Anonymous Access</label> <label class="col-sm-4 col-form-label">Anonymous Access</label>
<div class="col-sm-10"> <div class="col-sm-7">
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked= {{model.general.allowAnonymousAccess}} <input type="checkbox" class="form-check-input" id="allowAnonymousAccess" checked= {{model.general.allowAnonymousAccess}}
/> />
@ -36,8 +36,8 @@
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="conversionEndpoint" class="col-sm-2 col-form-label">Conversion Service URL</label> <label for="conversionEndpoint" class="col-sm-4 col-form-label">Conversion Service URL</label>
<div class="col-sm-10"> <div class="col-sm-7">
{{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError 'form-control is-invalid' 'form-control')}} {{input id="conversionEndpoint" type="text" value=model.general.conversionEndpoint class=(if hasConversionEndpointInputError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted"> <small class="form-text text-muted">
Endpoint for handling import/export (e.g. https://api.documize.com, Endpoint for handling import/export (e.g. https://api.documize.com,
@ -45,6 +45,22 @@
</small> </small>
</div> </div>
</div> </div>
<div class="btn btn-success mt-4" {{action 'save'}}>Save</div> <div class="form-group row">
<label for="maxTags" class="col-sm-4 col-form-label">Maximum Tags Per Document</label>
<div class="col-sm-7">
<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>
</div>
<div class="btn btn-success font-weight-bold text-uppercase mt-4" {{action 'save'}}>Save</div>
</form> </form>
</div> </div>

View file

@ -2,27 +2,17 @@
<div class="explainer-header">Categories & Tags</div> <div class="explainer-header">Categories & Tags</div>
<p class="explainer-text explainer-gap">Categorize your content, assign tags to suppliment</p> <p class="explainer-text explainer-gap">Categorize your content, assign tags to suppliment</p>
<h1>Specify up to three tags</h1> <h1>Specify up to {{appMeta.maxTags}} tags</h1>
<p class="form-text text-muted">Lowercase, characters, numbers, hyphens only</p> <p class="form-text text-muted">Lowercase, characters, numbers, hyphens only</p>
<form> <form>
<div class="input-group mb-3"> {{#each tagz as |tag|}}
<div class="input-group-prepend"> <div class="input-group mb-3">
<span class="input-group-text">#</span> <div class="input-group-prepend">
<span class="input-group-text">#</span>
</div>
{{input type='text' id=(concat 'add-tag-field-' tag.number) class="form-control mousetrap tag-input" placeholder="Tag name" value=tag.value}}
</div> </div>
{{input type='text' id='add-tag-field1' class="form-control mousetrap tag-input" placeholder="Tag name" value=tag1}} {{/each}}
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">#</span>
</div>
{{input type='text' id='add-tag-field2' class="form-control mousetrap tag-input" placeholder="Tag name" value=tag2}}
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">#</span>
</div>
{{input type='text' id='add-tag-field3' class="form-control mousetrap tag-input" placeholder="Tag name" value=tag3}}
</div>
</form> </form>
<div class="mt-5" /> <div class="mt-5" />

View file

@ -75,7 +75,7 @@
<td>{{x-toggle value=permission.spaceView onToggle=(action (mut permission.spaceView))}}</td> <td>{{x-toggle value=permission.spaceView onToggle=(action (mut permission.spaceView))}}</td>
<td>{{x-toggle value=permission.spaceManage onToggle=(action (mut permission.spaceManage))}}</td> <td>{{x-toggle value=permission.spaceManage onToggle=(action (mut permission.spaceManage))}}</td>
<td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner))}}</td> <td>{{x-toggle value=permission.spaceOwner onToggle=(action (mut permission.spaceOwner))}}</td>
<td>{{x-toggle value=permission.documentAdd onToggle=(action (mut permission.spacdocumentAddView))}}</td> <td>{{x-toggle value=permission.documentAdd onToggle=(action (mut permission.documentAdd))}}</td>
<td>{{x-toggle value=permission.documentEdit onToggle=(action (mut permission.documentEdit))}}</td> <td>{{x-toggle value=permission.documentEdit onToggle=(action (mut permission.documentEdit))}}</td>
<td>{{x-toggle value=permission.documentDelete onToggle=(action (mut permission.documentDelete))}}</td> <td>{{x-toggle value=permission.documentDelete onToggle=(action (mut permission.documentDelete))}}</td>
<td>{{x-toggle value=permission.documentMove onToggle=(action (mut permission.documentMove))}}</td> <td>{{x-toggle value=permission.documentMove onToggle=(action (mut permission.documentMove))}}</td>

View file

@ -1,6 +1,6 @@
{ {
"name": "documize", "name": "documize",
"version": "1.66.0", "version": "1.67.0",
"description": "The Document IDE", "description": "The Document IDE",
"private": true, "private": true,
"repository": "", "repository": "",

View file

@ -32,6 +32,7 @@ type SiteMeta struct {
AuthProvider string `json:"authProvider"` AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"` AuthConfig string `json:"authConfig"`
Version string `json:"version"` Version string `json:"version"`
MaxTags int `json:"maxTags"`
Edition string `json:"edition"` Edition string `json:"edition"`
Valid bool `json:"valid"` Valid bool `json:"valid"`
ConversionEndpoint string `json:"conversionEndpoint"` ConversionEndpoint string `json:"conversionEndpoint"`

View file

@ -26,6 +26,7 @@ type Organization struct {
AuthProvider string `json:"authProvider"` AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"` AuthConfig string `json:"authConfig"`
ConversionEndpoint string `json:"conversionEndpoint"` ConversionEndpoint string `json:"conversionEndpoint"`
MaxTags int `json:"maxTags"`
Serial string `json:"-"` Serial string `json:"-"`
Active bool `json:"-"` Active bool `json:"-"`
} }