diff --git a/client/components/cards/cardCustomFields.jade b/client/components/cards/cardCustomFields.jade index 11f1bc2f6..6fa1ac90e 100644 --- a/client/components/cards/cardCustomFields.jade +++ b/client/components/cards/cardCustomFields.jade @@ -119,3 +119,24 @@ template(name="cardCustomField-dropdown") if value +viewer = selectedItem + +template(name="cardCustomField-stringtemplate") + if canModifyCard + +inlinedForm(classNames="js-card-customfield-stringtemplate") + each item in stringtemplateItems.get + input.js-card-customfield-stringtemplate-item(type="text" value=item placeholder="") + input.js-card-customfield-stringtemplate-item.last(type="text" value="" placeholder="{{_ 'custom-field-stringtemplate-item-placeholder'}}") + .edit-controls.clearfix + button.primary(type="submit") {{_ 'save'}} + a.fa.fa-times-thin.js-close-inlined-form + else + a.js-open-inlined-form + if value + +viewer + = formattedValue + else + | {{_ 'edit'}} + else + if value + +viewer + = formattedValue diff --git a/client/components/cards/cardCustomFields.js b/client/components/cards/cardCustomFields.js index 4469e221e..832379432 100644 --- a/client/components/cards/cardCustomFields.js +++ b/client/components/cards/cardCustomFields.js @@ -234,3 +234,86 @@ CardCustomField.register('cardCustomField'); ]; } }.register('cardCustomField-dropdown')); + +// cardCustomField-stringtemplate +(class extends CardCustomField { + onCreated() { + super.onCreated(); + + this.stringtemplateFormat = this.data().definition.settings.stringtemplateFormat; + this.stringtemplateSeparator = this.data().definition.settings.stringtemplateSeparator; + + this.stringtemplateItems = new ReactiveVar(this.data().value ?? []); + } + + formattedValue() { + return (this.data().value ?? []) + .filter(value => !!value.trim()) + .map(value => this.stringtemplateFormat.replace(/%\{value\}/gi, value)) + .join(this.stringtemplateSeparator ?? ''); + } + + getItems() { + return Array.from(this.findAll('input')) + .map(input => input.value) + .filter(value => !!value.trim()); + } + + events() { + return [ + { + 'submit .js-card-customfield-stringtemplate'(event) { + event.preventDefault(); + const items = this.getItems(); + this.card.setCustomField(this.customFieldId, items); + }, + + 'keydown .js-card-customfield-stringtemplate-item'(event) { + if (event.keyCode === 13) { + event.preventDefault(); + + if (event.metaKey || event.ctrlKey) { + this.find('button[type=submit]').click(); + } else if (event.target.value.trim()) { + const inputLast = this.find('input.last'); + + let items = this.getItems(); + + if (event.target === inputLast) { + inputLast.value = ''; + } else if (event.target.nextSibling === inputLast) { + inputLast.focus(); + } else { + event.target.blur(); + + const idx = Array.from(this.findAll('input')) + .indexOf(event.target); + items.splice(idx + 1, 0, ''); + + Tracker.afterFlush(() => { + const element = this.findAll('input')[idx + 1]; + element.focus(); + element.value = ''; + }); + } + + this.stringtemplateItems.set(items); + } + } + }, + + 'blur .js-card-customfield-stringtemplate-item'(event) { + if (!event.target.value.trim() || event.target === this.find('input.last')) { + const items = this.getItems(); + this.stringtemplateItems.set(items); + this.find('input.last').value = ''; + } + }, + + 'click .js-close-inlined-form'(event) { + this.stringtemplateItems.set(this.data().value ?? []); + }, + }, + ]; + } +}.register('cardCustomField-stringtemplate')); diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 3931bffbc..cc37cd383 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -82,6 +82,9 @@ template(name="minicard") +minicardCustomFieldDate else if $eq definition.type "checkbox" .materialCheckBox(class="{{#if value }}is-checked{{/if}}") + else if $eq definition.type "stringtemplate" + +viewer + = formattedStringtemplateCustomFieldValue(definition) else +viewer = trueValue diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 88348fc48..ff2fa640c 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -21,6 +21,20 @@ BlazeComponent.extendComponent({ }).format(customFieldTrueValue); }, + formattedStringtemplateCustomFieldValue(definition) { + const customField = this.data() + .customFieldsWD() + .find(f => f._id === definition._id); + + const customFieldTrueValue = + customField && customField.trueValue ? customField.trueValue : []; + + return customFieldTrueValue + .filter(value => !!value.trim()) + .map(value => definition.settings.stringtemplateFormat.replace(/%\{value\}/gi, value)) + .join(definition.settings.stringtemplateSeparator ?? ''); + }, + events() { return [ { diff --git a/client/components/sidebar/sidebarCustomFields.jade b/client/components/sidebar/sidebarCustomFields.jade index 3f7a3e2ee..d1970ef1a 100644 --- a/client/components/sidebar/sidebarCustomFields.jade +++ b/client/components/sidebar/sidebarCustomFields.jade @@ -50,6 +50,15 @@ template(name="createCustomFieldPopup") each dropdownItems.get input.js-dropdown-item(type="text" value=name placeholder="") input.js-dropdown-item.last(type="text" value="" placeholder="{{_ 'custom-field-dropdown-options-placeholder'}}") + + div.js-field-settings.js-field-settings-stringtemplate(class="{{#if isTypeNotSelected 'stringtemplate'}}hide{{/if}}") + label + | {{_ 'custom-field-stringtemplate-format'}} + input.js-field-stringtemplate-format(type="text" value=getStringtemplateFormat) + label + | {{_ 'custom-field-stringtemplate-separator'}} + input.js-field-stringtemplate-separator(type="text" value=getStringtemplateSeparator) + a.flex.js-field-show-on-card(class="{{#if showOnCard}}is-checked{{/if}}") .materialCheckBox(class="{{#if showOnCard}}is-checked{{/if}}") diff --git a/client/components/sidebar/sidebarCustomFields.js b/client/components/sidebar/sidebarCustomFields.js index 24385f72a..2f1fa7dd6 100644 --- a/client/components/sidebar/sidebarCustomFields.js +++ b/client/components/sidebar/sidebarCustomFields.js @@ -16,7 +16,15 @@ BlazeComponent.extendComponent({ }).register('customFieldsSidebar'); const CreateCustomFieldPopup = BlazeComponent.extendComponent({ - _types: ['text', 'number', 'date', 'dropdown', 'currency', 'checkbox'], + _types: [ + 'text', + 'number', + 'date', + 'dropdown', + 'currency', + 'checkbox', + 'stringtemplate', + ], _currencyList: [ { @@ -77,6 +85,18 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ ? this.data().settings.dropdownItems : [], ); + + this.stringtemplateFormat = new ReactiveVar( + this.data().settings && this.data().settings.stringtemplateFormat + ? this.data().settings.stringtemplateFormat + : '', + ); + + this.stringtemplateSeparator = new ReactiveVar( + this.data().settings && this.data().settings.stringtemplateSeparator + ? this.data().settings.stringtemplateSeparator + : '', + ); }, types() { @@ -121,6 +141,14 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ return items; }, + getStringtemplateFormat() { + return this.stringtemplateFormat.get(); + }, + + getStringtemplateSeparator() { + return this.stringtemplateSeparator.get(); + }, + getSettings() { const settings = {}; switch (this.type.get()) { @@ -136,6 +164,14 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ settings.dropdownItems = dropdownItems; break; } + case 'stringtemplate': { + const stringtemplateFormat = this.stringtemplateFormat.get(); + settings.stringtemplateFormat = stringtemplateFormat; + + const stringtemplateSeparator = this.stringtemplateSeparator.get(); + settings.stringtemplateSeparator = stringtemplateSeparator; + break; + } } return settings; }, @@ -158,6 +194,14 @@ const CreateCustomFieldPopup = BlazeComponent.extendComponent({ evt.target.value = ''; } }, + 'input .js-field-stringtemplate-format'(evt) { + const value = evt.target.value; + this.stringtemplateFormat.set(value); + }, + 'input .js-field-stringtemplate-separator'(evt) { + const value = evt.target.value; + this.stringtemplateSeparator.set(value); + }, 'click .js-field-show-on-card'(evt) { let $target = $(evt.target); if (!$target.hasClass('js-field-show-on-card')) { diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index e20645c51..847352fc4 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -988,5 +988,9 @@ "hide-system-messages-of-all-users": "Hide system messages of all users", "now-system-messages-of-all-users-are-hidden": "Now system messages of all users are hidden", "move-swimlane": "Move Swimlane", - "moveSwimlanePopup-title": "Move Swimlane" + "moveSwimlanePopup-title": "Move Swimlane", + "custom-field-stringtemplate": "String Template", + "custom-field-stringtemplate-format": "Format (use %{value} as placeholder)", + "custom-field-stringtemplate-separator": "Separator (use or   for a space)", + "custom-field-stringtemplate-item-placeholder": "Press enter to add more items" } diff --git a/models/cards.js b/models/cards.js index a0aed5c69..f0e33b22f 100644 --- a/models/cards.js +++ b/models/cards.js @@ -155,10 +155,14 @@ Cards.attachSchema( /** * value attached to the custom field */ - type: Match.OneOf(String, Number, Boolean, Date), + type: Match.OneOf(String, Number, Boolean, Date, [String]), optional: true, defaultValue: '', }, + 'value.$': { + type: String, + optional: true, + }, }), }, dateLastActivity: { diff --git a/models/customFields.js b/models/customFields.js index 5f3150174..9f5db0667 100644 --- a/models/customFields.js +++ b/models/customFields.js @@ -29,6 +29,7 @@ CustomFields.attachSchema( 'dropdown', 'checkbox', 'currency', + 'stringtemplate', ], }, settings: { @@ -64,6 +65,14 @@ CustomFields.attachSchema( }, }), }, + 'settings.stringtemplateFormat': { + type: String, + optional: true, + }, + 'settings.stringtemplateSeparator': { + type: String, + optional: true, + }, showOnCard: { /** * should we show on the cards this custom field