bullet_train 1.0.93 → 1.0.95

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fd0ed98cb437a6a91f7289ee15c050bbf07ad8321c2eb5cd62e6095bc58b67e
4
- data.tar.gz: 7f380b03457f3a8f68d87451a731c7dc0c3c67fbfe1de2ac9bc0984f5a6e5075
3
+ metadata.gz: 40a00b0c6312326ea0ec3622729494460a0cfc90a51533daa823e55579c5a7dc
4
+ data.tar.gz: f6198fb630ec3d5f2b54614d34e6542e6f783286b94d2eab9ec300ec0df09223
5
5
  SHA512:
6
- metadata.gz: b56c26291a63a669de8526e03b60473ea44589f562efb85ba0e100c8267b9d8b1e6b728acb1fcfe71b97a0f9789d5e24451b04a2609ab9e7f9941970195825d9
7
- data.tar.gz: 95a757e8187c5b0d0f0108794796b1b73a2d8f8b3edccf20c9040b6e6308ba594f650b73cb5576f41080dcd9f9bc34a154a2de8b9bbd106858a81bd347cfa59d
6
+ metadata.gz: 0e7cd38b24aac43e77bf974bee30b2444c693539f91acf25a0f98832984e424ce7fb96f9ad88e057138eb80c41740ebfe88f088e33abe301af7d5352223a7587
7
+ data.tar.gz: 10c2473995e1e1eec0f57e82f9d9182df3d4cb3486b4b1298daefa80e439bb2f4cede431e41476bb8df1bf1db5e5715d8e7b21009dd4ff265320b8e0da4c204c
@@ -1,2 +1,2 @@
1
- import{Controller as e}from"@hotwired/stimulus";function t(e){const t=(e.match(/^(?:\.\/)?(.+)(?:[_-]controller\..+?)$/)||[])[1];if(t)return t.replace(/_/g,"-").replace(/\//g,"--")}class l extends e{connect(){this.updateAvailability()}updateFormAndSubmit(e){return this.recreateIdsHiddenFields(),this.createOrUpdateAllField(),!0}updateIds(e){var t;null!=e&&null!=(t=e.detail)&&t.ids&&(this.idsValue=e.detail.ids,this.allValue=e.detail.all),this.updateAvailability(),this.updateButtonLabel()}updateAvailability(){this.element.classList.toggle(this.hiddenClass,0===this.idsValue.length)}updateButtonLabel(){let e=this.buttonIfAllValue;this.idsValue.length&&!1===this.allValue&&(e=this.buttonIfIdsValue.replace("{num}",this.idsValue.length)),"INPUT"===this.buttonTarget.tagName?this.buttonTarget.value=e:this.buttonTarget.textContent=e}recreateIdsHiddenFields(){this.removeIdsHiddenFields(),this.createIdsHiddenFields()}removeIdsHiddenFields(){this.idsHiddenFieldTargets.forEach(e=>{this.element.removeChild(e)})}createIdsHiddenFields(){this.idsValue.forEach(e=>{let t=document.createElement("input");t.type="hidden",t.name=this.objectNameValue+"["+this.idsFieldNameValue+"][]",t.value=e,this.element.appendChild(t)})}createOrUpdateAllField(){this.hasAllHiddenFieldTarget?this.allHiddenFieldTarget.value=this.allValue?"true":"false":this.createAllField()}createAllField(){let e=document.createElement("input");e.type="hidden",e.name=this.objectNameValue+"["+this.allFieldNameValue+"]",e.value=this.allValue?"true":"false",this.element.appendChild(e)}}l.targets=["button","idsHiddenField","allHiddenField"],l.classes=["hidden"],l.values={buttonIfAll:String,buttonIfIds:String,ids:Array,all:Boolean,objectName:String,idsFieldName:String,allFieldName:String};class s extends e{connect(){this.element.classList.add(this.selectableAvailableClass)}toggleSelectable(){this.selectableValue=!this.selectableValue}updateSelectedIds(){this.updateActions(),this.updateSelectAllCheckbox()}updateActions(){this.actionTargets.forEach(e=>{e.dispatchEvent(new CustomEvent("update-ids",{detail:{ids:this.selectedIds,all:this.allSelected}}))})}selectAllOrNone(e){this.allSelected?this.selectNone():this.selectAll(),this.updateSelectAllCheckbox()}selectAll(){this.checkboxTargets.forEach(e=>{e.checked=!0}),this.updateActions()}selectNone(){this.checkboxTargets.forEach(e=>{e.checked=!1}),this.updateActions()}updateSelectAllCheckbox(){let e=this.selectAllCheckboxTarget,t=this.selectAllLabelTarget;this.allSelected?(e.checked=!0,e.indeterminate=!1,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!0}}))):this.selectedIds.length>0?(e.indeterminate=!0,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!1}}))):(e.checked=!1,e.indeterminate=!1,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!1}})))}selectableValueChanged(){this.element.classList.toggle(this.selectableClass,this.selectableValue),this.updateToggleLabel()}updateToggleLabel(){this.selectableToggleTarget.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:this.selectableValue}}))}get selectedIds(){let e=[];return this.checkboxTargets.forEach(t=>{t.checked&&e.push(t.value)}),e}get allSelected(){return this.selectedIds.length===this.checkboxTargets.length}}s.targets=["checkbox","selectAllCheckbox","action","selectableToggle","selectAllLabel"],s.classes=["selectableAvailable","selectable"],s.values={selectable:Boolean};class i extends e{copy(){this.inputTarget.value=this.sourceTarget.innerText,this.inputTarget.select(),document.execCommand("copy"),this.buttonTarget.innerHTML='<i id="copied" class="fas fa-check w-4 h-4 block text-green-600"></i>',setTimeout(function(){document.getElementById("copied").innerHTML='<i class="far fa-copy w-4 h-4 block text-gray-600"></i>'},1500)}}i.targets=["source","input","button"];class a extends e{constructor(){super(...arguments),this.removeTrailingNewlines=e=>{e.element.innerHTML.match(/<br><\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-10)+"</div>",this.removeTrailingNewlines(e))},this.removeTrailingWhitespace=e=>{e.element.innerHTML.match(/&nbsp;<\/div>$/)?(e.element.innerHTML=e.element.innerHTML.slice(0,-12)+"</div>",this.removeTrailingWhitespace(e)):e.element.innerHTML.match(/&nbsp; <\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-13)+"</div>",this.removeTrailingWhitespace(e))}}resetOnSuccess(e){e.detail.success&&e.target.reset()}stripTrix(){this.trixFieldTargets.forEach(e=>{this.removeTrailingNewlines(e.editor),this.removeTrailingWhitespace(e.editor),e.parentElement.querySelector("input").value=e.innerHTML})}submitOnReturn(e){if((e.metaKey||e.ctrlKey)&&13==e.keyCode){e.preventDefault();let t=e.target.closest("form");this.submitForm(t)}}submitForm(e){e.requestSubmit?e.requestSubmit():e.querySelector("[type=submit]").click()}}a.targets=["trixField","scroll"];class n extends e{toggle(){const e=this.isWrapperHidden?this.showEventNameValue:this.hideEventNameValue;this.isWrapperHidden&&this.showWrapper(),this.wrapperTarget.dispatchEvent(new CustomEvent(e))}get isWrapperHidden(){return this.wrapperTarget.classList.contains(this.hiddenClass)}showWrapper(){this.wrapperTarget.classList.remove(this.hiddenClass)}hideWrapper(){this.wrapperTarget.classList.add(this.hiddenClass)}}n.targets=["wrapper"],n.classes=["hidden"],n.values={showEventName:String,hideEventName:String};const r=[[l,"bulk_action_form_controller.js"],[s,"bulk_actions_controller.js"],[i,"clipboard_controller.js"],[a,"form_controller.js"],[n,"mobile_menu_controller.js"]].map(function(e){const l=e[0];return{identifier:t(e[1]),controllerConstructor:l}});document.addEventListener("turbo:load",()=>{navigator.userAgent.toLocaleLowerCase().includes("electron")&&document.body.classList.add("electron")});export{r as controllerDefinitions};
1
+ import{identifierForContextKey as e}from"@hotwired/stimulus-webpack-helpers";import{Controller as t}from"@hotwired/stimulus";class l extends t{connect(){this.updateAvailability()}updateFormAndSubmit(e){return this.recreateIdsHiddenFields(),this.createOrUpdateAllField(),!0}updateIds(e){var t;null!=e&&null!=(t=e.detail)&&t.ids&&(this.idsValue=e.detail.ids,this.allValue=e.detail.all),this.updateAvailability(),this.updateButtonLabel()}updateAvailability(){this.element.classList.toggle(this.hiddenClass,0===this.idsValue.length)}updateButtonLabel(){let e=this.buttonIfAllValue;this.idsValue.length&&!1===this.allValue&&(e=this.buttonIfIdsValue.replace("{num}",this.idsValue.length)),"INPUT"===this.buttonTarget.tagName?this.buttonTarget.value=e:this.buttonTarget.textContent=e}recreateIdsHiddenFields(){this.removeIdsHiddenFields(),this.createIdsHiddenFields()}removeIdsHiddenFields(){this.idsHiddenFieldTargets.forEach(e=>{this.element.removeChild(e)})}createIdsHiddenFields(){this.idsValue.forEach(e=>{let t=document.createElement("input");t.type="hidden",t.name=this.objectNameValue+"["+this.idsFieldNameValue+"][]",t.value=e,this.element.appendChild(t)})}createOrUpdateAllField(){this.hasAllHiddenFieldTarget?this.allHiddenFieldTarget.value=this.allValue?"true":"false":this.createAllField()}createAllField(){let e=document.createElement("input");e.type="hidden",e.name=this.objectNameValue+"["+this.allFieldNameValue+"]",e.value=this.allValue?"true":"false",this.element.appendChild(e)}}l.targets=["button","idsHiddenField","allHiddenField"],l.classes=["hidden"],l.values={buttonIfAll:String,buttonIfIds:String,ids:Array,all:Boolean,objectName:String,idsFieldName:String,allFieldName:String};class s extends t{connect(){this.element.classList.add(this.selectableAvailableClass)}toggleSelectable(){this.selectableValue=!this.selectableValue}updateSelectedIds(){this.updateActions(),this.updateSelectAllCheckbox()}updateActions(){this.actionTargets.forEach(e=>{e.dispatchEvent(new CustomEvent("update-ids",{detail:{ids:this.selectedIds,all:this.allSelected}}))})}selectAllOrNone(e){this.allSelected?this.selectNone():this.selectAll(),this.updateSelectAllCheckbox()}selectAll(){this.checkboxTargets.forEach(e=>{e.checked=!0}),this.updateActions()}selectNone(){this.checkboxTargets.forEach(e=>{e.checked=!1}),this.updateActions()}updateSelectAllCheckbox(){let e=this.selectAllCheckboxTarget,t=this.selectAllLabelTarget;this.allSelected?(e.checked=!0,e.indeterminate=!1,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!0}}))):this.selectedIds.length>0?(e.indeterminate=!0,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!1}}))):(e.checked=!1,e.indeterminate=!1,t.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:!1}})))}selectableValueChanged(){this.element.classList.toggle(this.selectableClass,this.selectableValue),this.updateToggleLabel()}updateToggleLabel(){this.selectableToggleTarget.dispatchEvent(new CustomEvent("toggle",{detail:{useAlternate:this.selectableValue}}))}get selectedIds(){let e=[];return this.checkboxTargets.forEach(t=>{t.checked&&e.push(t.value)}),e}get allSelected(){return this.selectedIds.length===this.checkboxTargets.length}}s.targets=["checkbox","selectAllCheckbox","action","selectableToggle","selectAllLabel"],s.classes=["selectableAvailable","selectable"],s.values={selectable:Boolean};class i extends t{copy(){this.inputTarget.value=this.sourceTarget.innerText,this.inputTarget.select(),document.execCommand("copy"),this.buttonTarget.innerHTML='<i id="copied" class="fas fa-check w-4 h-4 block text-green-600"></i>',setTimeout(function(){document.getElementById("copied").innerHTML='<i class="far fa-copy w-4 h-4 block text-gray-600"></i>'},1500)}}i.targets=["source","input","button"];class a extends t{constructor(){super(...arguments),this.removeTrailingNewlines=e=>{e.element.innerHTML.match(/<br><\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-10)+"</div>",this.removeTrailingNewlines(e))},this.removeTrailingWhitespace=e=>{e.element.innerHTML.match(/&nbsp;<\/div>$/)?(e.element.innerHTML=e.element.innerHTML.slice(0,-12)+"</div>",this.removeTrailingWhitespace(e)):e.element.innerHTML.match(/&nbsp; <\/div>$/)&&(e.element.innerHTML=e.element.innerHTML.slice(0,-13)+"</div>",this.removeTrailingWhitespace(e))}}resetOnSuccess(e){e.detail.success&&e.target.reset()}stripTrix(){this.trixFieldTargets.forEach(e=>{this.removeTrailingNewlines(e.editor),this.removeTrailingWhitespace(e.editor),e.parentElement.querySelector("input").value=e.innerHTML})}submitOnReturn(e){if((e.metaKey||e.ctrlKey)&&13==e.keyCode){e.preventDefault();let t=e.target.closest("form");this.submitForm(t)}}submitForm(e){e.requestSubmit?e.requestSubmit():e.querySelector("[type=submit]").click()}}a.targets=["trixField","scroll"];class n extends t{toggle(){const e=this.isWrapperHidden?this.showEventNameValue:this.hideEventNameValue;this.isWrapperHidden&&this.showWrapper(),this.wrapperTarget.dispatchEvent(new CustomEvent(e))}get isWrapperHidden(){return this.wrapperTarget.classList.contains(this.hiddenClass)}showWrapper(){this.wrapperTarget.classList.remove(this.hiddenClass)}hideWrapper(){this.wrapperTarget.classList.add(this.hiddenClass)}}n.targets=["wrapper"],n.classes=["hidden"],n.values={showEventName:String,hideEventName:String};const r=[[l,"bulk_action_form_controller.js"],[s,"bulk_actions_controller.js"],[i,"clipboard_controller.js"],[a,"form_controller.js"],[n,"mobile_menu_controller.js"]].map(function(t){const l=t[0];return{identifier:e(t[1]),controllerConstructor:l}});document.addEventListener("turbo:load",()=>{navigator.userAgent.toLocaleLowerCase().includes("electron")&&document.body.classList.add("electron")});export{r as controllerDefinitions};
2
2
  //# sourceMappingURL=bullet-train.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bullet-train.js","sources":["../../../node_modules/@hotwired/stimulus-webpack-helpers/dist/stimulus-webpack-helpers.js","../../javascript/controllers/bulk_action_form_controller.js","../../javascript/controllers/bulk_actions_controller.js","../../javascript/controllers/clipboard_controller.js","../../javascript/controllers/form_controller.js","../../javascript/controllers/mobile_menu_controller.js","../../javascript/controllers/index.js","../../javascript/electron/index.js"],"sourcesContent":["/*\nStimulus Webpack Helpers 1.0.0\nCopyright © 2021 Basecamp, LLC\n */\nfunction definitionsFromContext(context) {\n return context.keys()\n .map((key) => definitionForModuleWithContextAndKey(context, key))\n .filter((value) => value);\n}\nfunction definitionForModuleWithContextAndKey(context, key) {\n const identifier = identifierForContextKey(key);\n if (identifier) {\n return definitionForModuleAndIdentifier(context(key), identifier);\n }\n}\nfunction definitionForModuleAndIdentifier(module, identifier) {\n const controllerConstructor = module.default;\n if (typeof controllerConstructor == \"function\") {\n return { identifier, controllerConstructor };\n }\n}\nfunction identifierForContextKey(key) {\n const logicalName = (key.match(/^(?:\\.\\/)?(.+)(?:[_-]controller\\..+?)$/) || [])[1];\n if (logicalName) {\n return logicalName.replace(/_/g, \"-\").replace(/\\//g, \"--\");\n }\n}\n\nexport { definitionForModuleAndIdentifier, definitionForModuleWithContextAndKey, definitionsFromContext, identifierForContextKey };\n","import { Controller } from 'stimulus'\n\nexport default class extends Controller {\n static targets = [ \"button\", \"idsHiddenField\", \"allHiddenField\" ]\n static classes = [ \"hidden\" ]\n static values = {\n buttonIfAll: String,\n buttonIfIds: String,\n ids: Array,\n all: Boolean,\n objectName: String,\n idsFieldName: String,\n allFieldName: String\n }\n\n connect() {\n this.updateAvailability()\n }\n\n updateFormAndSubmit(event) {\n this.recreateIdsHiddenFields()\n this.createOrUpdateAllField()\n return true\n }\n\n updateIds(event) {\n if (event?.detail?.ids) {\n this.idsValue = event.detail.ids\n this.allValue = event.detail.all\n }\n\n this.updateAvailability()\n this.updateButtonLabel()\n }\n\n updateAvailability() {\n this.element.classList.toggle(this.hiddenClass, this.idsValue.length === 0)\n }\n\n updateButtonLabel() {\n let label = this.buttonIfAllValue\n if (this.idsValue.length && this.allValue === false) {\n label = this.buttonIfIdsValue.replace('{num}', this.idsValue.length)\n }\n\n switch (this.buttonTarget.tagName) {\n case 'INPUT': this.buttonTarget.value = label; break;\n default: this.buttonTarget.textContent = label; break;\n }\n }\n\n recreateIdsHiddenFields() {\n this.removeIdsHiddenFields()\n this.createIdsHiddenFields()\n }\n\n removeIdsHiddenFields() {\n this.idsHiddenFieldTargets.forEach(field => {\n this.element.removeChild(field)\n })\n }\n\n createIdsHiddenFields() {\n this.idsValue.forEach(id => {\n let field = document.createElement('input')\n field.type = 'hidden'\n field.name = `${this.objectNameValue}[${this.idsFieldNameValue}][]`\n field.value = id\n this.element.appendChild(field)\n })\n }\n\n createOrUpdateAllField() {\n if (this.hasAllHiddenFieldTarget) {\n this.allHiddenFieldTarget.value = this.allValue? 'true': 'false'\n } else {\n this.createAllField()\n }\n }\n\n createAllField() {\n let field = document.createElement('input')\n field.type = 'hidden'\n field.name = `${this.objectNameValue}[${this.allFieldNameValue}]`\n field.value = this.allValue? 'true': 'false'\n this.element.appendChild(field)\n }\n}","import { Controller } from 'stimulus'\n\nexport default class extends Controller {\n static targets = [ \"checkbox\", \"selectAllCheckbox\", \"action\", \"selectableToggle\", \"selectAllLabel\" ]\n static classes = [ \"selectableAvailable\", \"selectable\" ]\n static values = {\n selectable: Boolean\n }\n\n connect() {\n this.element.classList.add(this.selectableAvailableClass)\n }\n\n toggleSelectable() {\n this.selectableValue = !this.selectableValue\n }\n\n updateSelectedIds() {\n this.updateActions()\n this.updateSelectAllCheckbox()\n }\n\n updateActions() {\n this.actionTargets.forEach(actionTarget => {\n actionTarget.dispatchEvent(new CustomEvent('update-ids', { detail: {\n ids: this.selectedIds,\n all: this.allSelected\n }}))\n })\n }\n\n selectAllOrNone(event) {\n if (this.allSelected) {\n this.selectNone()\n } else {\n this.selectAll()\n }\n this.updateSelectAllCheckbox()\n }\n\n selectAll() {\n this.checkboxTargets.forEach(checkbox => {\n checkbox.checked = true\n })\n this.updateActions()\n }\n\n selectNone() {\n this.checkboxTargets.forEach(checkbox => {\n checkbox.checked = false\n })\n this.updateActions()\n }\n\n updateSelectAllCheckbox() {\n let checkbox = this.selectAllCheckboxTarget\n let label = this.selectAllLabelTarget\n\n if (this.allSelected) {\n checkbox.checked = true\n checkbox.indeterminate = false\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: true }} ))\n } else if (this.selectedIds.length > 0) {\n checkbox.indeterminate = true\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: false }} ))\n } else {\n checkbox.checked = false\n checkbox.indeterminate = false\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: false }} ))\n }\n }\n\n selectableValueChanged() {\n this.element.classList.toggle(this.selectableClass, this.selectableValue)\n this.updateToggleLabel()\n }\n\n updateToggleLabel() {\n this.selectableToggleTarget.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: this.selectableValue }} ))\n }\n\n get selectedIds() {\n let ids = []\n this.checkboxTargets.forEach(checkbox => {\n if (checkbox.checked) {\n ids.push(checkbox.value)\n }\n })\n return ids\n }\n\n get allSelected() {\n return this.selectedIds.length === this.checkboxTargets.length\n }\n}","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = ['source', 'input', 'button']\n\n copy() {\n this.inputTarget.value = this.sourceTarget.innerText\n this.inputTarget.select()\n document.execCommand('copy')\n this.buttonTarget.innerHTML = '<i id=\"copied\" class=\"fas fa-check w-4 h-4 block text-green-600\"></i>'\n setTimeout(function () {\n document.getElementById('copied').innerHTML = '<i class=\"far fa-copy w-4 h-4 block text-gray-600\"></i>'\n }, 1500)\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\n// TODO Some of this feels really specific to the conversation messages form. Should we rename this controller?\nexport default class extends Controller {\n static targets = ['trixField', 'scroll']\n\n resetOnSuccess(e){\n if(e.detail.success) {\n e.target.reset();\n }\n }\n\n stripTrix(){\n this.trixFieldTargets.forEach(element => {\n this.removeTrailingNewlines(element.editor)\n this.removeTrailingWhitespace(element.editor)\n // When doing this as part of the form submission, Trix does not update the input element's value attribute fast enough.\n // In order to submit the stripped value, we manually update it here to fix the race condition\n element.parentElement.querySelector(\"input\").value = element.innerHTML\n })\n }\n\n submitOnReturn(e) {\n if((e.metaKey || e.ctrlKey) && e.keyCode == 13) {\n e.preventDefault();\n let form = e.target.closest(\"form\")\n this.submitForm(form)\n }\n }\n\n removeTrailingNewlines = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/<br><\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -10) + \"</div>\"\n this.removeTrailingNewlines(trixEditor)\n }\n }\n\n removeTrailingWhitespace = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/&nbsp;<\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/&nbsp; <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -13) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n }\n }\n\n submitForm(form) {\n // Right now, Safari and IE don't support the requestSubmit method which is required for Turbo\n // Doing form.submit() doesn't actually fire the submit event which Turbo needs\n if (form.requestSubmit) {\n form.requestSubmit()\n } else {\n form.querySelector(\"[type=submit]\").click()\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"wrapper\"]\n static classes = [ \"hidden\" ] // necessary because stimulus-reveal will mess with the [hidden] attribute on the wrapper\n static values = {\n showEventName: String,\n hideEventName: String,\n }\n\n toggle() {\n const eventName = this.isWrapperHidden? this.showEventNameValue: this.hideEventNameValue\n if (this.isWrapperHidden) {\n this.showWrapper()\n }\n \n this.wrapperTarget.dispatchEvent(new CustomEvent(eventName))\n }\n \n get isWrapperHidden() {\n return this.wrapperTarget.classList.contains(this.hiddenClass)\n }\n \n showWrapper() {\n this.wrapperTarget.classList.remove(this.hiddenClass)\n }\n \n hideWrapper() {\n this.wrapperTarget.classList.add(this.hiddenClass)\n }\n}","import { identifierForContextKey } from \"@hotwired/stimulus-webpack-helpers\"\n\nimport BulkActionFormController from './bulk_action_form_controller'\nimport BulkActionsController from './bulk_actions_controller'\nimport ClipboardController from './clipboard_controller'\nimport FormController from './form_controller'\nimport MobileMenuController from './mobile_menu_controller'\n\nexport const controllerDefinitions = [\n [BulkActionFormController, 'bulk_action_form_controller.js'],\n [BulkActionsController, 'bulk_actions_controller.js'],\n [ClipboardController, 'clipboard_controller.js'],\n [FormController, 'form_controller.js'],\n [MobileMenuController, 'mobile_menu_controller.js'],\n].map(function(d) {\n const key = d[1]\n const controller = d[0]\n return {\n identifier: identifierForContextKey(key),\n controllerConstructor: controller\n }\n})\n","document.addEventListener(\"turbo:load\", () => {\n if (navigator.userAgent.toLocaleLowerCase().includes('electron')) {\n document.body.classList.add('electron')\n }\n})"],"names":["identifierForContextKey","key","logicalName","match","replace","Controller","connect","this","updateAvailability","updateFormAndSubmit","event","recreateIdsHiddenFields","createOrUpdateAllField","updateIds","detail","_event$detail","ids","idsValue","allValue","all","updateButtonLabel","element","classList","toggle","hiddenClass","length","label","buttonIfAllValue","buttonIfIdsValue","buttonTarget","tagName","value","textContent","removeIdsHiddenFields","createIdsHiddenFields","idsHiddenFieldTargets","forEach","field","removeChild","id","document","createElement","type","name","objectNameValue","idsFieldNameValue","appendChild","hasAllHiddenFieldTarget","allHiddenFieldTarget","createAllField","allFieldNameValue","targets","classes","values","buttonIfAll","String","buttonIfIds","Array","Boolean","objectName","idsFieldName","allFieldName","add","selectableAvailableClass","toggleSelectable","selectableValue","updateSelectedIds","updateActions","updateSelectAllCheckbox","actionTargets","actionTarget","dispatchEvent","CustomEvent","selectedIds","allSelected","selectAllOrNone","selectNone","selectAll","checkboxTargets","checkbox","checked","selectAllCheckboxTarget","selectAllLabelTarget","indeterminate","useAlternate","selectableValueChanged","selectableClass","updateToggleLabel","selectableToggleTarget","push","selectable","copy","inputTarget","sourceTarget","innerText","select","execCommand","innerHTML","setTimeout","getElementById","removeTrailingNewlines","trixEditor","slice","removeTrailingWhitespace","resetOnSuccess","e","success","target","reset","stripTrix","trixFieldTargets","editor","parentElement","querySelector","submitOnReturn","metaKey","ctrlKey","keyCode","preventDefault","form","closest","submitForm","requestSubmit","click","eventName","isWrapperHidden","showEventNameValue","hideEventNameValue","showWrapper","wrapperTarget","contains","remove","hideWrapper","showEventName","hideEventName","controllerDefinitions","BulkActionFormController","BulkActionsController","ClipboardController","FormController","MobileMenuController","map","d","controller","identifier","controllerConstructor","addEventListener","navigator","userAgent","toLocaleLowerCase","includes","body"],"mappings":"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"}
1
+ {"version":3,"file":"bullet-train.js","sources":["../../javascript/controllers/bulk_action_form_controller.js","../../javascript/controllers/bulk_actions_controller.js","../../javascript/controllers/clipboard_controller.js","../../javascript/controllers/form_controller.js","../../javascript/controllers/mobile_menu_controller.js","../../javascript/controllers/index.js","../../javascript/electron/index.js"],"sourcesContent":["import { Controller } from '@hotwired/stimulus'\n\nexport default class extends Controller {\n static targets = [ \"button\", \"idsHiddenField\", \"allHiddenField\" ]\n static classes = [ \"hidden\" ]\n static values = {\n buttonIfAll: String,\n buttonIfIds: String,\n ids: Array,\n all: Boolean,\n objectName: String,\n idsFieldName: String,\n allFieldName: String\n }\n\n connect() {\n this.updateAvailability()\n }\n\n updateFormAndSubmit(event) {\n this.recreateIdsHiddenFields()\n this.createOrUpdateAllField()\n return true\n }\n\n updateIds(event) {\n if (event?.detail?.ids) {\n this.idsValue = event.detail.ids\n this.allValue = event.detail.all\n }\n\n this.updateAvailability()\n this.updateButtonLabel()\n }\n\n updateAvailability() {\n this.element.classList.toggle(this.hiddenClass, this.idsValue.length === 0)\n }\n\n updateButtonLabel() {\n let label = this.buttonIfAllValue\n if (this.idsValue.length && this.allValue === false) {\n label = this.buttonIfIdsValue.replace('{num}', this.idsValue.length)\n }\n\n switch (this.buttonTarget.tagName) {\n case 'INPUT': this.buttonTarget.value = label; break;\n default: this.buttonTarget.textContent = label; break;\n }\n }\n\n recreateIdsHiddenFields() {\n this.removeIdsHiddenFields()\n this.createIdsHiddenFields()\n }\n\n removeIdsHiddenFields() {\n this.idsHiddenFieldTargets.forEach(field => {\n this.element.removeChild(field)\n })\n }\n\n createIdsHiddenFields() {\n this.idsValue.forEach(id => {\n let field = document.createElement('input')\n field.type = 'hidden'\n field.name = `${this.objectNameValue}[${this.idsFieldNameValue}][]`\n field.value = id\n this.element.appendChild(field)\n })\n }\n\n createOrUpdateAllField() {\n if (this.hasAllHiddenFieldTarget) {\n this.allHiddenFieldTarget.value = this.allValue? 'true': 'false'\n } else {\n this.createAllField()\n }\n }\n\n createAllField() {\n let field = document.createElement('input')\n field.type = 'hidden'\n field.name = `${this.objectNameValue}[${this.allFieldNameValue}]`\n field.value = this.allValue? 'true': 'false'\n this.element.appendChild(field)\n }\n}","import { Controller } from '@hotwired/stimulus'\n\nexport default class extends Controller {\n static targets = [ \"checkbox\", \"selectAllCheckbox\", \"action\", \"selectableToggle\", \"selectAllLabel\" ]\n static classes = [ \"selectableAvailable\", \"selectable\" ]\n static values = {\n selectable: Boolean\n }\n\n connect() {\n this.element.classList.add(this.selectableAvailableClass)\n }\n\n toggleSelectable() {\n this.selectableValue = !this.selectableValue\n }\n\n updateSelectedIds() {\n this.updateActions()\n this.updateSelectAllCheckbox()\n }\n\n updateActions() {\n this.actionTargets.forEach(actionTarget => {\n actionTarget.dispatchEvent(new CustomEvent('update-ids', { detail: {\n ids: this.selectedIds,\n all: this.allSelected\n }}))\n })\n }\n\n selectAllOrNone(event) {\n if (this.allSelected) {\n this.selectNone()\n } else {\n this.selectAll()\n }\n this.updateSelectAllCheckbox()\n }\n\n selectAll() {\n this.checkboxTargets.forEach(checkbox => {\n checkbox.checked = true\n })\n this.updateActions()\n }\n\n selectNone() {\n this.checkboxTargets.forEach(checkbox => {\n checkbox.checked = false\n })\n this.updateActions()\n }\n\n updateSelectAllCheckbox() {\n let checkbox = this.selectAllCheckboxTarget\n let label = this.selectAllLabelTarget\n\n if (this.allSelected) {\n checkbox.checked = true\n checkbox.indeterminate = false\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: true }} ))\n } else if (this.selectedIds.length > 0) {\n checkbox.indeterminate = true\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: false }} ))\n } else {\n checkbox.checked = false\n checkbox.indeterminate = false\n label.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: false }} ))\n }\n }\n\n selectableValueChanged() {\n this.element.classList.toggle(this.selectableClass, this.selectableValue)\n this.updateToggleLabel()\n }\n\n updateToggleLabel() {\n this.selectableToggleTarget.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: this.selectableValue }} ))\n }\n\n get selectedIds() {\n let ids = []\n this.checkboxTargets.forEach(checkbox => {\n if (checkbox.checked) {\n ids.push(checkbox.value)\n }\n })\n return ids\n }\n\n get allSelected() {\n return this.selectedIds.length === this.checkboxTargets.length\n }\n}","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = ['source', 'input', 'button']\n\n copy() {\n this.inputTarget.value = this.sourceTarget.innerText\n this.inputTarget.select()\n document.execCommand('copy')\n this.buttonTarget.innerHTML = '<i id=\"copied\" class=\"fas fa-check w-4 h-4 block text-green-600\"></i>'\n setTimeout(function () {\n document.getElementById('copied').innerHTML = '<i class=\"far fa-copy w-4 h-4 block text-gray-600\"></i>'\n }, 1500)\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\n// TODO Some of this feels really specific to the conversation messages form. Should we rename this controller?\nexport default class extends Controller {\n static targets = ['trixField', 'scroll']\n\n resetOnSuccess(e){\n if(e.detail.success) {\n e.target.reset();\n }\n }\n\n stripTrix(){\n this.trixFieldTargets.forEach(element => {\n this.removeTrailingNewlines(element.editor)\n this.removeTrailingWhitespace(element.editor)\n // When doing this as part of the form submission, Trix does not update the input element's value attribute fast enough.\n // In order to submit the stripped value, we manually update it here to fix the race condition\n element.parentElement.querySelector(\"input\").value = element.innerHTML\n })\n }\n\n submitOnReturn(e) {\n if((e.metaKey || e.ctrlKey) && e.keyCode == 13) {\n e.preventDefault();\n let form = e.target.closest(\"form\")\n this.submitForm(form)\n }\n }\n\n removeTrailingNewlines = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/<br><\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -10) + \"</div>\"\n this.removeTrailingNewlines(trixEditor)\n }\n }\n\n removeTrailingWhitespace = (trixEditor) => {\n if (trixEditor.element.innerHTML.match(/&nbsp;<\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/&nbsp; <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -13) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n }\n }\n\n submitForm(form) {\n // Right now, Safari and IE don't support the requestSubmit method which is required for Turbo\n // Doing form.submit() doesn't actually fire the submit event which Turbo needs\n if (form.requestSubmit) {\n form.requestSubmit()\n } else {\n form.querySelector(\"[type=submit]\").click()\n }\n }\n}\n","import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"wrapper\"]\n static classes = [ \"hidden\" ] // necessary because stimulus-reveal will mess with the [hidden] attribute on the wrapper\n static values = {\n showEventName: String,\n hideEventName: String,\n }\n\n toggle() {\n const eventName = this.isWrapperHidden? this.showEventNameValue: this.hideEventNameValue\n if (this.isWrapperHidden) {\n this.showWrapper()\n }\n \n this.wrapperTarget.dispatchEvent(new CustomEvent(eventName))\n }\n \n get isWrapperHidden() {\n return this.wrapperTarget.classList.contains(this.hiddenClass)\n }\n \n showWrapper() {\n this.wrapperTarget.classList.remove(this.hiddenClass)\n }\n \n hideWrapper() {\n this.wrapperTarget.classList.add(this.hiddenClass)\n }\n}","import { identifierForContextKey } from \"@hotwired/stimulus-webpack-helpers\"\n\nimport BulkActionFormController from './bulk_action_form_controller'\nimport BulkActionsController from './bulk_actions_controller'\nimport ClipboardController from './clipboard_controller'\nimport FormController from './form_controller'\nimport MobileMenuController from './mobile_menu_controller'\n\nexport const controllerDefinitions = [\n [BulkActionFormController, 'bulk_action_form_controller.js'],\n [BulkActionsController, 'bulk_actions_controller.js'],\n [ClipboardController, 'clipboard_controller.js'],\n [FormController, 'form_controller.js'],\n [MobileMenuController, 'mobile_menu_controller.js'],\n].map(function(d) {\n const key = d[1]\n const controller = d[0]\n return {\n identifier: identifierForContextKey(key),\n controllerConstructor: controller\n }\n})\n","document.addEventListener(\"turbo:load\", () => {\n if (navigator.userAgent.toLocaleLowerCase().includes('electron')) {\n document.body.classList.add('electron')\n }\n})"],"names":["_class","connect","this","updateAvailability","updateFormAndSubmit","event","recreateIdsHiddenFields","createOrUpdateAllField","updateIds","_event$detail","detail","ids","idsValue","allValue","all","updateButtonLabel","element","classList","toggle","hiddenClass","length","label","buttonIfAllValue","buttonIfIdsValue","replace","buttonTarget","tagName","value","textContent","removeIdsHiddenFields","createIdsHiddenFields","idsHiddenFieldTargets","forEach","field","removeChild","id","document","createElement","type","name","objectNameValue","idsFieldNameValue","appendChild","hasAllHiddenFieldTarget","allHiddenFieldTarget","createAllField","allFieldNameValue","targets","classes","values","buttonIfAll","String","buttonIfIds","Array","Boolean","objectName","idsFieldName","allFieldName","Controller","add","selectableAvailableClass","toggleSelectable","selectableValue","updateSelectedIds","updateActions","updateSelectAllCheckbox","actionTargets","actionTarget","dispatchEvent","selectedIds","allSelected","selectAllOrNone","selectNone","selectAll","checkboxTargets","checkbox","checked","selectAllCheckboxTarget","selectAllLabelTarget","indeterminate","CustomEvent","useAlternate","selectableValueChanged","selectableClass","updateToggleLabel","selectableToggleTarget","push","selectable","copy","inputTarget","sourceTarget","innerText","select","execCommand","innerHTML","setTimeout","getElementById","removeTrailingNewlines","trixEditor","match","slice","removeTrailingWhitespace","resetOnSuccess","e","success","target","reset","stripTrix","trixFieldTargets","editor","parentElement","querySelector","submitOnReturn","metaKey","ctrlKey","keyCode","preventDefault","form","closest","submitForm","requestSubmit","click","isWrapperHidden","showEventNameValue","hideEventNameValue","showWrapper","wrapperTarget","eventName","contains","remove","hideWrapper","showEventName","hideEventName","controllerDefinitions","BulkActionFormController","BulkActionsController","ClipboardController","FormController","MobileMenuController","map","d","identifier","identifierForContextKey","controllerConstructor","controller","addEventListener","navigator","userAgent","toLocaleLowerCase","includes","body"],"mappings":"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"}
@@ -42,9 +42,13 @@ module Account::Invitations::ControllerBase
42
42
  # POST /invitations/1/accept
43
43
  # POST /invitations/1/accept.json
44
44
  def accept
45
+ # The user should be alerted when there is no
46
+ # invitation regardless if they're registered or not.
47
+ @invitation = Invitation.find_by(uuid: params[:id])
48
+ flash[:alert] = t("invitations.notifications.doesnt_exist") if @invitation.nil?
49
+
45
50
  # unless the user is signed in.
46
51
  if !current_user.present?
47
-
48
52
  # keep track of the uuid of the invitation so we can reload it
49
53
  # after they sign up. at this point we don't even know if it's
50
54
  # valid, but that's fine.
@@ -56,22 +60,16 @@ module Account::Invitations::ControllerBase
56
60
  # assume the user needs to create an account.
57
61
  # this is not the default for devise, but a sensible default here.
58
62
  redirect_to new_user_registration_path
59
-
60
- else
61
-
62
- @invitation = Invitation.find_by(uuid: params[:id])
63
-
64
- if @invitation
65
- @team = @invitation.team
66
- if @invitation.is_for?(current_user) || request.post?
67
- @invitation.accept_for(current_user)
68
- redirect_to account_dashboard_path, notice: I18n.t("invitations.notifications.welcome", team_name: @team.name)
69
- else
70
- redirect_to account_invitation_path(@invitation.uuid)
71
- end
63
+ elsif @invitation
64
+ @team = @invitation.team
65
+ if @invitation.is_for?(current_user) || request.post?
66
+ @invitation.accept_for(current_user)
67
+ redirect_to account_dashboard_path, notice: I18n.t("invitations.notifications.welcome", team_name: @team.name)
72
68
  else
73
- redirect_to account_dashboard_path
69
+ redirect_to account_invitation_path(@invitation.uuid)
74
70
  end
71
+ else
72
+ redirect_to account_dashboard_path
75
73
 
76
74
  end
77
75
  end
@@ -1,3 +1,14 @@
1
1
  class SessionsController < Devise::SessionsController
2
2
  include Sessions::ControllerBase
3
+
4
+ def destroy
5
+ if params.include?(:onboard_logout)
6
+ signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
7
+ set_flash_message! :notice, :signed_out if signed_out
8
+ yield if block_given?
9
+ redirect_to root_path
10
+ else
11
+ super
12
+ end
13
+ end
3
14
  end
@@ -1,4 +1,4 @@
1
- import { Controller } from 'stimulus'
1
+ import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
4
  static targets = [ "button", "idsHiddenField", "allHiddenField" ]
@@ -1,4 +1,4 @@
1
- import { Controller } from 'stimulus'
1
+ import { Controller } from '@hotwired/stimulus'
2
2
 
3
3
  export default class extends Controller {
4
4
  static targets = [ "checkbox", "selectAllCheckbox", "action", "selectableToggle", "selectAllLabel" ]
@@ -12,5 +12,6 @@ module Mailers::Base
12
12
  helper "account/users"
13
13
  helper "account/locale"
14
14
  helper "fields/trix_editor"
15
+ helper "theme"
15
16
  end
16
17
  end
@@ -62,7 +62,7 @@ module Memberships::Base
62
62
  end
63
63
 
64
64
  def tombstone?
65
- user.nil? && invitation.nil?
65
+ user.nil? && invitation.nil? && !platform_agent?
66
66
  end
67
67
 
68
68
  def last_admin?
@@ -138,4 +138,8 @@ module Memberships::Base
138
138
  def should_receive_notifications?
139
139
  invitation.present? || user.present?
140
140
  end
141
+
142
+ def platform_agent?
143
+ platform_agent_of_id.present?
144
+ end
141
145
  end
@@ -66,8 +66,7 @@ module Users::Base
66
66
 
67
67
  def create_default_team
68
68
  # This creates a `Membership`, because `User` `has_many :teams, through: :memberships`
69
- # TODO The team name should take into account the user's current locale.
70
- default_team = teams.create(name: "Your Team", time_zone: time_zone)
69
+ default_team = teams.create(name: I18n.t("teams.new.default_team_name"), time_zone: time_zone)
71
70
  memberships.find_by(team: default_team).update role_ids: [Role.admin.id]
72
71
  update(current_team: default_team)
73
72
  end
@@ -46,7 +46,9 @@
46
46
  <% else %>
47
47
  <%= link_to t('.buttons.promote'), [:promote, :account, @membership], method: :post, data: { confirm: t('global.confirm_message') }, class: first_button_primary if can? :promote, @membership %>
48
48
  <% end %>
49
- <%= button_to t(".buttons.#{membership_destroy_locale_key(@membership)}"), [:account, @membership], method: :delete, data: { confirm: t(".buttons.confirmations.#{membership_destroy_locale_key(@membership)}", model_locales(@membership)) }, class: first_button_primary if can? :destroy, @membership %>
49
+ <% if (can? :destroy, @membership) && (!@membership.platform_agent?) %>
50
+ <%= button_to t(".buttons.#{membership_destroy_locale_key(@membership)}"), [:account, @membership], method: :delete, data: { confirm: t(".buttons.confirmations.#{membership_destroy_locale_key(@membership)}", model_locales(@membership)) }, class: first_button_primary %>
51
+ <% end %>
50
52
  <% end %>
51
53
 
52
54
  <%= link_to t('global.buttons.back'), [:account, @team, :memberships], class: first_button_primary %>
@@ -38,7 +38,7 @@
38
38
  <% if other_teams.any? %>
39
39
  <%= link_to t('global.buttons.back'), main_app.account_teams_path, class: first_button_primary %>
40
40
  <% else %>
41
- <%= link_to t('menus.main.labels.logout'), main_app.destroy_user_session_path, class: first_button_primary, method: 'delete' %>
41
+ <%= link_to t('menus.main.labels.logout'), main_app.destroy_user_session_path(@user, onboard_logout: true), class: first_button_primary, method: 'delete' %>
42
42
  <% end %>
43
43
  </div>
44
44
  <% end %>
@@ -0,0 +1,43 @@
1
+ en:
2
+ framework_packages:
3
+ bullet_train:
4
+ git: "bullet-train-co/bullet_train-base"
5
+ npm: "@bullet-train/bullet-train"
6
+ bullet_train-api:
7
+ git: "bullet-train-co/bullet_train-api"
8
+ bullet_train-fields:
9
+ git: "bullet-train-co/bullet_train-fields"
10
+ npm: "@bullet-train/fields"
11
+ bullet_train-has_uuid:
12
+ git: "bullet-train-co/bullet_train-has_uuid"
13
+ bullet_train-incoming_webhooks:
14
+ git: "bullet-train-co/bullet_train-incoming_webhooks"
15
+ bullet_train-integrations:
16
+ git: "bullet-train-co/bullet_train-integrations"
17
+ bullet_train-integrations-stripe:
18
+ git: "bullet-train-co/bullet_train-base-integrations-stripe"
19
+ bullet_train-obfuscates_id:
20
+ git: "bullet-train-co/bullet_train-obfuscates_id"
21
+ bullet_train-outgoing_webhooks:
22
+ git: "bullet-train-co/bullet_train-outgoing_webhooks"
23
+ bullet_train-outgoing_webhooks-core:
24
+ git: "bullet-train-co/bullet_train-outgoing_webhooks-core"
25
+ bullet_train-scope_questions:
26
+ git: "bullet-train-co/bullet_train-scope_questions"
27
+ bullet_train-scope_validator:
28
+ git: "bullet-train-co/bullet_train-scope_validator"
29
+ bullet_train-sortable:
30
+ git: "bullet-train-co/bullet_train-sortable"
31
+ npm: "@bullet-train/bullet-train-sortable"
32
+ bullet_train-super_scaffolding:
33
+ git: "bullet-train-co/bullet_train-super_scaffolding"
34
+ bullet_train-super_load_and_authorize_resource:
35
+ git: "bullet-train-co/bullet_train-super_load_and_authorize_resource"
36
+ bullet_train-themes:
37
+ git: "bullet-train-co/bullet_train-themes"
38
+ bullet_train-themes-base:
39
+ git: "bullet-train-co/bullet_train-themes-base"
40
+ bullet_train-themes-light:
41
+ git: "bullet-train-co/bullet_train-themes-light"
42
+ bullet_train-themes-tailwind_css:
43
+ git: "bullet-train-co/bullet_train-themes-tailwind_css"
@@ -56,6 +56,7 @@ en:
56
56
  created: 'Invitation was successfully created.'
57
57
  updated: 'Invitation was successfully updated.'
58
58
  destroyed: 'Invitation was successfully destroyed.'
59
+ doesnt_exist: Sorry, but we couldn't find your invitation. Please contact an administrator on the team to have them send you another one.
59
60
  values:
60
61
  name: 'Invitation to Join %{team_name}'
61
62
  account:
@@ -21,6 +21,7 @@ en:
21
21
  new:
22
22
  header: Create a New Team
23
23
  former_user_header: Join a Team
24
+ default_team_name: Your Team
24
25
  buttons: *buttons
25
26
  you_can: "You can either:"
26
27
  invitation_header: Accept an invitation
@@ -56,3 +56,14 @@ Here is the same example, with search enabled:
56
56
  choices: @project.valid_memberships.map { |membership| [membership.name, membership.id] },
57
57
  html_options: {multiple: true}, other_options: {search: true} %>
58
58
  </code></pre>
59
+
60
+ ## Accepting New Entries
61
+
62
+ Here is an example allowing a new option to be entered by the user:
63
+
64
+ <pre><code><%= render 'shared/fields/super_select', form: form, method: :delay_minutes,
65
+ choices: %w(1 5 10 30).map { |value| [value, value] },
66
+ other_options: {accepts_new: true} %>
67
+ </code></pre>
68
+
69
+ Note: this will set the option `value` (which will be submitted to the server) to the entered text.
data/docs/indirection.md CHANGED
@@ -38,7 +38,32 @@ If you're looking at a rendered view in the browser, it can be hard to know whic
38
38
  <!-- BEGIN /Users/andrewculver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bullet_train-themes-light-1.0.10/app/views/themes/light/workflow/_box.html.erb -->
39
39
  ```
40
40
 
41
- Note that in the example above, the view in question isn't actually coming from the application repository. Instead, it's being included from the `bullet_train-themes-light` package. For instructions on how to customize it, see [Overriding Framework Defaults](/docs/overriding.md).
41
+ If you want to customize files like this that you find annotated in your browser, you can use the `--interactive` flag to eject the file to your main application, or simply open it in your code editor.
42
+
43
+ ```
44
+ > bin/resolve --interactive
45
+
46
+ OK, paste what you've got for us and hit <Return>!
47
+
48
+ <!-- BEGIN /Users/andrewculver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bullet_train-themes-light-1.0.10/app/views/themes/light/workflow/_box.html.erb -->
49
+
50
+ Absolute path:
51
+ /Users/andrewculver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bullet_train-themes-light-1.0.10/app/views/themes/light/workflow/_box.html.erb
52
+
53
+ Package name:
54
+ bullet_train-themes-light-1.0.10
55
+
56
+
57
+ Would you like to eject the file into the local project? (y/n)
58
+ n
59
+
60
+ Would you like to open `/Users/andrewculver/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bullet_train-themes-light-1.0.10/app/views/themes/light/workflow/_box.html.erb`? (y/n)
61
+ y
62
+ ```
63
+
64
+ You may also want to consider using `bin/develop`, which will clone the Bullet Train package of your choice to `local/` within your main application's root directory. Running this command will also automatically link the package to your main application and open it in the code editor for you, so you can start using the cloned repository and make changes to your main application right away.
65
+
66
+ Note that in the example above, the view in question isn't actually coming from the application repository. Instead, it's being included from the `bullet_train-themes-light` package. For further instructions on how to customize it, see [Overriding Framework Defaults](/docs/overriding.md).
42
67
 
43
68
  ### Drilling Down on Translation Keys
44
69
 
data/docs/onboarding.md CHANGED
@@ -13,29 +13,29 @@ The "user email" onboarding step is specifically used in situations where a user
13
13
 
14
14
  ## Relevant Files
15
15
 
16
- > TODO This section needs to be updated now that a bunch of these files are coming in from gems.
16
+ Each of the following files exist within their own respective Bullet Train packages. Make sure you navigate to the correct package when searching for the following files.
17
17
 
18
18
  ### Controllers
19
- - `ensure_onboarding_is_complete` in `app/controllers/account/application_controller.rb`
20
- - `app/controllers/account/onboarding/user_details_controller.rb`
21
- - `app/controllers/account/onboarding/user_email_controller.rb`
19
+ - `ensure_onboarding_is_complete` in `bullet_train/app/controllers/account/application_controller.rb`
20
+ - `bullet_train-base/app/controllers/account/onboarding/user_details_controller.rb`
21
+ - `bullet_train-base/app/controllers/account/onboarding/user_email_controller.rb`
22
22
 
23
23
  ### Views
24
- - `app/views/account/onboarding/user_details/edit.html.erb`
25
- - `app/views/account/onboarding/user_email/edit.html.erb`
24
+ - `bullet_train-base/app/views/account/onboarding/user_details/edit.html.erb`
25
+ - `bullet_train-base/app/views/account/onboarding/user_email/edit.html.erb`
26
26
 
27
27
  ### Models
28
- - `user#details_provided?` in `app/models/user.rb`
28
+ - `user#details_provided?` in `bullet_train-base/app/models/concerns/users/base.rb`
29
29
 
30
30
  ### Routes
31
- - `namespace :onboarding` in `config/routes.rb`
31
+ - `namespace :onboarding` in `bullet_train-base/config/routes.rb` and `bullet_train/config/routes.rb`
32
32
 
33
33
  ## Adding Additional Steps
34
34
  Although you can implement onboarding steps from scratch, we always just copy and paste one of the existing steps as a starting point, like so:
35
35
 
36
36
  1. Copy, rename, and modify of the existing onboarding controllers.
37
37
  2. Copy, rename, and modify the corresponding `edit.html.erb` view.
38
- 3. Copy and rename the route entry in `config/routes.rb`.
39
- 4. Add the appropriate gating logic in `ensure_onboarding_is_complete` in `app/controllers/account/application_controller.rb`
38
+ 3. Copy and rename the route entry in `bullet_train-base/config/routes.rb`.
39
+ 4. Add the appropriate gating logic in `ensure_onboarding_is_complete` in `bullet_train/app/controllers/account/application_controller.rb`
40
40
 
41
41
  Onboarding steps aren't limited to targeting `User` models. It's possible to add onboarding steps to help flesh out team `Membership` records or `Team` records as well. You can use this pattern for setting up any sort of required data for either the user or the team.
@@ -0,0 +1,137 @@
1
+ # Super Scaffolding with the `--sortable` option
2
+
3
+ When issuing a `bin/super-scaffold crud` command, you can pass the `--sortable` option like this:
4
+
5
+ ```
6
+ # E.g. Pages belong to a Site and are sortable via drag-and-drop:
7
+ rails g model Page site:references name:string path:text
8
+ bin/super-scaffold crud Page Site,Team name:text path:text --sortable
9
+ ```
10
+
11
+ The `--sortable` option:
12
+
13
+ 1. Wraps the table's body in a `sortable` Stimulus controller, providing drag-and-drop re-ordering;
14
+ 2. Adds a `reorder` action to your resource via `include SortableActions`, triggered automatically on re-order;
15
+ 3. Adds a migration to add the `sort_order` column to your model to store the ordering;
16
+ 4. Adds a `default_scope` which orders by `sort_order` and auto increments `sort_order` on create via `include Sortable` on the model.
17
+
18
+ ## Disabling Saving on Re-order
19
+
20
+ By default, a call to save the new `sort_order` is triggered automatically on re-order.
21
+
22
+ ### To disable auto-saving
23
+
24
+ Add the `data-sortable-save-on-reorder-value="false"` param on the `sortable` root element:
25
+
26
+ ```html
27
+ <tbody data-controller="sortable"
28
+ data-sortable-save-on-reorder-value="false"
29
+ ...
30
+ >
31
+ ```
32
+
33
+ ### To manually fire the save action via a button
34
+
35
+ Since the button won't be part of the `sortable` root element's descendants (all its direct descendants are sortable by default), you'll need to wrap both the `sortable` element and the save button in a new Stimulus controlled ancestor element.
36
+
37
+ ```js
38
+ /* sortable-wrapper_controller.js */
39
+ import { Controller } from "@hotwired/stimulus"
40
+
41
+ export default class extends Controller {
42
+ static targets = [ "sortable" ]
43
+
44
+ saveSortOrder() {
45
+ if (!this.hasSortableTarget) { return }
46
+ this.sortableTarget.dispatchEvent(new CustomEvent("save-sort-order"))
47
+ }
48
+ }
49
+ ```
50
+
51
+ On the button, add a `data-action`
52
+
53
+ ```html
54
+ <button data-action="sortable-wrapper#saveSortOrder">Save Sort Order</button>
55
+ ```
56
+
57
+ And on the `sortable` element, catch the `save-sort-order` event and define it as the `sortable` target for the `sortable-wrapper` controller:
58
+
59
+ ```html
60
+ <tbody data-controller="sortable"
61
+ data-sortable-save-on-reorder-value="false"
62
+ data-action="save-sort-order->sortable#saveSortOrder"
63
+ data-sortable-wrapper-target="sortable"
64
+ ...
65
+ >
66
+ ```
67
+
68
+ ## Events
69
+
70
+ Under the hood, the `sortable` Stimulus controller uses the [dragula](https://github.com/bevacqua/dragula) library.
71
+
72
+ All of the events that `dragula` defines are re-dispatched as native DOM events. The native DOM event name is prefixed with `sortable:`
73
+
74
+ | dragula event name | DOM event name |
75
+ |---------------------|----------------------|
76
+ | drag | sortable:drag |
77
+ | dragend | sortable:dragend |
78
+ | drop | sortable:drop |
79
+ | cancel | sortable:cancel |
80
+ | remove | sortable:remove |
81
+ | shadow | sortable:shadow |
82
+ | over | sortable:over |
83
+ | out | sortable:out |
84
+ | cloned | sortable:cloned |
85
+
86
+ The original event's listener arguments are passed to the native DOM event as a simple numbered Array under `event.detail.args`. See [dragula's list of events](https://github.com/bevacqua/dragula#drakeon-events) for the listener arguments.
87
+
88
+ ### Example: Asking for Confirmation on the `drop` Event
89
+
90
+ Let's say we'd like to ask the user to confirm before saving the new sort order:
91
+
92
+ > Are you sure you want to place DROPPED ITEM before SIBLING ITEM?
93
+
94
+ ```js
95
+ /* confirm-reorder_controller.js */
96
+ import { Controller } from "@hotwired/stimulus"
97
+
98
+ export default class extends Controller {
99
+ static targets = [ "sortable" ]
100
+
101
+ requestConfirmation(event) {
102
+ const [el, target, source, sibling] = event.detail?.args
103
+
104
+ // sibling will be undefined if dropped in last position, taking a shortcut here
105
+ const areYouSure = `Are you sure you want to place ${el.dataset.name} before ${sibling.dataset.name}?`
106
+
107
+ // let's suppose each <tr> in sortable has a data-name attribute
108
+ if (confirm(areYouSure)) {
109
+ this.sortableTarget.dispatchEvent(new CustomEvent('save-sort-order'))
110
+ } else {
111
+ this.revertToOriginalOrder()
112
+ }
113
+ }
114
+
115
+ prepareForRevertOnCancel(event) {
116
+ // we're assuming we can swap out the HTML safely
117
+ this.originalSortableHTML = this.sortableTarget.innerHTML
118
+ }
119
+
120
+ revertToOriginalOrder() {
121
+ if (this.originalSortableHTML === undefined) { return }
122
+ this.sortableTarget.innerHTML = this.originalSortableHTML
123
+ this.originalSortableHTML = undefined
124
+ }
125
+ }
126
+ ```
127
+
128
+ And on the `sortable` element, catch the `sortable:drop`, `sortable:drag` (for catching when dragging starts) and `save-sort-order` events. Also define it as the `sortable` target for the `confirm-reorder` controller:
129
+
130
+ ```html
131
+ <tbody data-controller="sortable"
132
+ data-sortable-save-on-reorder-value="false"
133
+ data-action="sortable:drop->confirm-reorder#requestConfirmation sortable:drag->confirm-reorder#prepareForRevertOnCancel save-sort-order->sortable#saveSortOrder"
134
+ data-confirm-reorder-target="sortable"
135
+ ...
136
+ >
137
+ ```
@@ -249,3 +249,4 @@ HIDE_THINGS: true
249
249
 
250
250
  ## Advanced Examples
251
251
  - [Super Scaffolding with Delegated Types](/docs/super-scaffolding/delegated-types.md)
252
+ - [Super Scaffolding with the `--sortable` option](/docs/super-scaffolding/sortable.md)
@@ -13,17 +13,14 @@ module BulletTrain
13
13
  source_file = calculate_source_file_details
14
14
 
15
15
  if source_file[:absolute_path]
16
+ puts ""
17
+ puts "Absolute path:".green
18
+ puts " #{source_file[:absolute_path]}".green
16
19
  puts ""
17
20
  if source_file[:package_name].present?
18
- puts "Absolute path:".green
19
- puts " #{source_file[:absolute_path]}".green
20
- puts ""
21
21
  puts "Package name:".green
22
22
  puts " #{source_file[:package_name]}".green
23
23
  else
24
- puts "Project path:".green
25
- puts " #{source_file[:project_path]}".green
26
- puts ""
27
24
  puts "Note: If this file was previously ejected from a package, we can no longer see which package it came from. However, it should say at the top of the file where it was ejected from.".yellow
28
25
  end
29
26
  puts ""
@@ -89,37 +86,28 @@ module BulletTrain
89
86
  package_name: nil,
90
87
  }
91
88
 
92
- result[:absolute_path] = class_path || partial_path || locale_path || file_path
93
-
89
+ result[:absolute_path] = file_path || class_path || partial_path || locale_path
94
90
  if result[:absolute_path]
95
- if result[:absolute_path].include?("devise")
96
- # The annotated path for devise doesn't actually return an absolute path,
97
- # so we have to do some extra steps to get the correct string.
98
- relative_partial_path = result[:absolute_path]
99
- # If it's a devise partial, it should be coming from bullet_train-base
100
- base_path = "#{`bundle show bullet_train`.chomp}/" + relative_partial_path
101
- result[:absolute_path] = base_path
102
- else
91
+ if result[:absolute_path].include?("/bullet_train")
103
92
  base_path = "bullet_train" + result[:absolute_path].partition("/bullet_train").last
104
- end
105
93
 
106
- # Try to calculate which package the file is from, and what it's path is within that project.
107
- ["app", "config", "lib"].each do |directory|
108
- regex = /\/#{directory}\//
109
- if base_path.match?(regex)
110
- project_path = "./#{directory}/#{base_path.rpartition(regex).last}"
111
- package_name = base_path.rpartition(regex).first.split("/").last
112
- # If the "package name" is actually just the local project directory.
113
- if package_name == `pwd`.chomp.split("/").last
114
- package_name = nil
115
- end
94
+ # Try to calculate which package the file is from, and what it's path is within that project.
95
+ ["app", "config", "lib"].each do |directory|
96
+ regex = /\/#{directory}\//
97
+ if base_path.match?(regex)
98
+ project_path = "./#{directory}/#{base_path.rpartition(regex).last}"
99
+ package_name = base_path.rpartition(regex).first.split("/").last
100
+ # If the "package name" is actually just the local project directory.
101
+ if package_name == `pwd`.chomp.split("/").last
102
+ package_name = nil
103
+ end
116
104
 
117
- result[:project_path] = project_path
118
- result[:package_name] = package_name
105
+ result[:project_path] = project_path
106
+ result[:package_name] = package_name
107
+ end
119
108
  end
120
109
  end
121
110
  end
122
-
123
111
  result
124
112
  end
125
113
 
@@ -135,9 +123,24 @@ module BulletTrain
135
123
  end
136
124
 
137
125
  def partial_path
138
- annotated_path = ApplicationController.render(template: "bullet_train/partial_resolver", layout: nil, assigns: {needle: @needle}).lines[1].chomp
126
+ begin
127
+ annotated_path = ApplicationController.render(template: "bullet_train/partial_resolver", layout: nil, assigns: {needle: @needle}).lines[1].chomp
128
+ rescue ActionView::Template::Error => e
129
+ # This is a really hacky way to get the file name, but the reason we're getting an error in the first place is because
130
+ # the partial requires locals that we aren't providing in the ApplicationController.render call above,
131
+ # resulting in an undefined local variable error. We do however get the file name, which we can pass back to the developer.
132
+ return e.file_name
133
+ end
134
+
139
135
  if annotated_path =~ /<!-- BEGIN (\S*) -->/
140
- $1
136
+ # If the developer enters a partial that is in bullet_train-base like devise/shared/oauth or devise/shared/links,
137
+ # it will return a string starting with app/ so we simply point them to the file in this repository.
138
+ if annotated_path.match?(/^<!-- BEGIN app/) && !ejected_theme?
139
+ gem_path = `bundle show bullet_train`.chomp
140
+ "#{gem_path}/#{$1}"
141
+ else
142
+ $1
143
+ end
141
144
  else
142
145
  raise "It looks like `config.action_view.annotate_rendered_view_with_filenames` isn't enabled?"
143
146
  end
@@ -168,5 +171,11 @@ module BulletTrain
168
171
 
169
172
  nil
170
173
  end
174
+
175
+ def ejected_theme?
176
+ current_theme_symbol = File.read("#{Rails.root}/app/helpers/application_helper.rb").split("\n").find { |str| str.match?(/\s+:.*/) }
177
+ current_theme = current_theme_symbol.delete(":").strip
178
+ current_theme != "light" && Dir.exist?("#{Rails.root}/app/assets/stylesheets/#{current_theme}")
179
+ end
171
180
  end
172
181
  end
@@ -1,3 +1,3 @@
1
1
  module BulletTrain
2
- VERSION = "1.0.93"
2
+ VERSION = "1.0.95"
3
3
  end
data/lib/bullet_train.rb CHANGED
@@ -18,8 +18,6 @@ require "cancancan"
18
18
  require "possessive"
19
19
  require "sidekiq"
20
20
  require "fastimage"
21
- require "pry"
22
- require "pry-stack_explorer"
23
21
  require "awesome_print"
24
22
  require "microscope"
25
23
  require "http_accept_language"
@@ -37,6 +35,8 @@ require "devise/pwned_password"
37
35
  module BulletTrain
38
36
  mattr_accessor :routing_concerns, default: []
39
37
  mattr_accessor :linked_gems, default: ["bullet_train"]
38
+ mattr_accessor :parent_class, default: "Team"
39
+ mattr_accessor :base_class, default: "ApplicationRecord"
40
40
  end
41
41
 
42
42
  def default_url_options_from_base_url
@@ -47,6 +47,12 @@ namespace :bullet_train do
47
47
  input = $1
48
48
  end
49
49
 
50
+ # Append the main application's path if the file is a local file.
51
+ # i.e. - app/views/layouts/_head.html.erb
52
+ if input.match?(/^app/)
53
+ input = "#{Rails.root}/#{input}"
54
+ end
55
+
50
56
  ARGV.unshift input.strip
51
57
  end
52
58
 
@@ -76,69 +82,7 @@ namespace :bullet_train do
76
82
  puts ""
77
83
  end
78
84
 
79
- # TODO Extract this into a YAML file.
80
- framework_packages = {
81
- "bullet_train" => {
82
- git: "bullet-train-co/bullet_train-base",
83
- npm: "@bullet-train/bullet-train"
84
- },
85
- "bullet_train-api" => {
86
- git: "bullet-train-co/bullet_train-api",
87
- },
88
- "bullet_train-fields" => {
89
- git: "bullet-train-co/bullet_train-fields",
90
- npm: "@bullet-train/fields"
91
- },
92
- "bullet_train-has_uuid" => {
93
- git: "bullet-train-co/bullet_train-has_uuid",
94
- },
95
- "bullet_train-incoming_webhooks" => {
96
- git: "bullet-train-co/bullet_train-incoming_webhooks",
97
- },
98
- "bullet_train-integrations" => {
99
- git: "bullet-train-co/bullet_train-integrations",
100
- },
101
- "bullet_train-integrations-stripe" => {
102
- git: "bullet-train-co/bullet_train-base-integrations-stripe",
103
- },
104
- "bullet_train-obfuscates_id" => {
105
- git: "bullet-train-co/bullet_train-obfuscates_id",
106
- },
107
- "bullet_train-outgoing_webhooks" => {
108
- git: "bullet-train-co/bullet_train-outgoing_webhooks",
109
- },
110
- "bullet_train-outgoing_webhooks-core" => {
111
- git: "bullet-train-co/bullet_train-outgoing_webhooks-core",
112
- },
113
- "bullet_train-scope_questions" => {
114
- git: "bullet-train-co/bullet_train-scope_questions",
115
- },
116
- "bullet_train-scope_validator" => {
117
- git: "bullet-train-co/bullet_train-scope_validator",
118
- },
119
- "bullet_train-sortable" => {
120
- git: "bullet-train-co/bullet_train-sortable",
121
- npm: "@bullet-train/bullet-train-sortable"
122
- },
123
- "bullet_train-super_scaffolding" => {
124
- git: "bullet-train-co/bullet_train-super_scaffolding",
125
- },
126
- "bullet_train-super_load_and_authorize_resource" => {
127
- git: "bullet-train-co/bullet_train-super_load_and_authorize_resource",
128
- },
129
- "bullet_train-themes" => {
130
- git: "bullet-train-co/bullet_train-themes",
131
- },
132
- "bullet_train-themes-base" => {
133
- git: "bullet-train-co/bullet_train-themes-base",
134
- },
135
- "bullet_train-themes-light" => {
136
- git: "bullet-train-co/bullet_train-themes-light",
137
- },
138
- "bullet_train-themes-tailwind_css" => {
139
- git: "bullet-train-co/bullet_train-themes-tailwind_css",
140
- },
141
- }
85
+ framework_packages = I18n.t("framework_packages")
142
86
 
143
87
  puts "Which framework package do you want to work on?".blue
144
88
  puts ""
@@ -156,23 +100,46 @@ namespace :bullet_train do
156
100
 
157
101
  puts "OK! Let's work on `#{gem}` together!".green
158
102
  puts ""
159
- puts "First, we're going to clone a copy of the package repository.".blue
160
-
161
- # TODO Prompt whether they want to check out their own forked version of the repository.
162
103
 
163
104
  if File.exist?("local/#{gem}")
164
- puts "Can't clone into `local/#{gem}` because it already exists. We will try to use what's already there.".yellow
165
- puts "However, it will be up to you to make sure that working copy of the repository is in a clean state and checked out to the `main` branch or whatever you want to work on.".yellow
166
- puts "Hit <Enter> to continue.".blue
167
- $stdin.gets
105
+ puts "We found the repository in `local/#{gem}`. We will try to use what's already there.".yellow
106
+ puts ""
107
+
108
+ # Adding these flags enables us to execute git commands in the gem from our starter repo.
109
+ work_tree_flag = "--work-tree=local/#{gem}"
110
+ git_dir_flag = "--git-dir=local/#{gem}/.git"
168
111
 
169
- # TODO We should check whether the local copy is in a clean state, and if it is, check out `main`.
170
- # TODO We should also pull `origin/main` to make sure we're on the most up-to-date version of the package.
112
+ git_status = `git #{work_tree_flag} #{git_dir_flag} status`
113
+ unless git_status.match?("nothing to commit, working tree clean")
114
+ puts "This package currently has uncommitted changes.".red
115
+ puts "Please make sure the branch is clean and try again.".red
116
+ exit
117
+ end
118
+
119
+ current_branch = (`git #{work_tree_flag} #{git_dir_flag} branch`).split("\n").select { |branch_name| branch_name.match?(/^\*\s/) }.pop.gsub(/^\*\s/, "")
120
+ unless current_branch == "main"
121
+ puts "Previously on #{current_branch}.".blue
122
+ puts "Switching local/#{gem} to main branch.".blue
123
+ stream("git #{work_tree_flag} #{git_dir_flag} checkout main")
124
+ end
125
+
126
+ puts "Updating the main branch with the latest changes.".blue
127
+ stream("git #{work_tree_flag} #{git_dir_flag} pull origin main")
171
128
  else
172
- stream "git clone git@github.com:#{details[:git]}.git local/#{gem}"
129
+ # Use https:// URLs when using this task in Gitpod.
130
+ stream "git clone #{`whoami`.chomp == "gitpod" ? "https://github.com/" : "git@github.com:"}#{details[:git]}.git local/#{gem}"
173
131
  end
174
132
 
175
- # TODO Ask them whether they want to check out a specific branch to work on. (List available remote branches.)
133
+ stream("git #{work_tree_flag} #{git_dir_flag} fetch")
134
+ stream("git #{work_tree_flag} #{git_dir_flag} branch -r")
135
+ puts "The above is a list of remote branches.".blue
136
+ puts "If there's one you'd like to work on, please enter the branch name and press <Enter>.".blue
137
+ puts "If not, just press <Enter> to continue.".blue
138
+ input = $stdin.gets.strip
139
+ unless input.empty?
140
+ puts "Switching to #{input.gsub("origin/", "")}".blue # TODO: Should we remove origin/ here if the developer types it?
141
+ stream("git #{work_tree_flag} #{git_dir_flag} checkout #{input}")
142
+ end
176
143
 
177
144
  puts ""
178
145
  puts "Now we'll try to link up that repository in the `Gemfile`.".blue
@@ -210,7 +177,7 @@ namespace :bullet_train do
210
177
  puts ""
211
178
  if details[:npm]
212
179
  puts "This package also has an npm package, so we'll link that up as well.".blue
213
- stream "cd local/#{gem} && yarn install && npx yalc link && cd ../.. && npx yalc link \"#{details[:npm]}\""
180
+ stream "cd local/#{gem} && yarn install && npm_config_yes=true npx yalc link && cd ../.. && npm_config_yes=true npx yalc link \"#{details[:npm]}\""
214
181
 
215
182
  puts ""
216
183
  puts "And now we're going to watch for any changes you make to the JavaScript and recompile as we go.".blue
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet_train
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.93
4
+ version: 1.0.95
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Culver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-03 00:00:00.000000000 Z
11
+ date: 2022-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: standard
@@ -192,34 +192,6 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
- - !ruby/object:Gem::Dependency
196
- name: pry
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - ">="
200
- - !ruby/object:Gem::Version
201
- version: '0'
202
- type: :runtime
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - ">="
207
- - !ruby/object:Gem::Version
208
- version: '0'
209
- - !ruby/object:Gem::Dependency
210
- name: pry-stack_explorer
211
- requirement: !ruby/object:Gem::Requirement
212
- requirements:
213
- - - ">="
214
- - !ruby/object:Gem::Version
215
- version: '0'
216
- type: :runtime
217
- prerelease: false
218
- version_requirements: !ruby/object:Gem::Requirement
219
- requirements:
220
- - - ">="
221
- - !ruby/object:Gem::Version
222
- version: '0'
223
195
  - !ruby/object:Gem::Dependency
224
196
  name: awesome_print
225
197
  requirement: !ruby/object:Gem::Requirement
@@ -402,6 +374,34 @@ dependencies:
402
374
  - - ">="
403
375
  - !ruby/object:Gem::Version
404
376
  version: '0'
377
+ - !ruby/object:Gem::Dependency
378
+ name: pry
379
+ requirement: !ruby/object:Gem::Requirement
380
+ requirements:
381
+ - - ">="
382
+ - !ruby/object:Gem::Version
383
+ version: '0'
384
+ type: :development
385
+ prerelease: false
386
+ version_requirements: !ruby/object:Gem::Requirement
387
+ requirements:
388
+ - - ">="
389
+ - !ruby/object:Gem::Version
390
+ version: '0'
391
+ - !ruby/object:Gem::Dependency
392
+ name: pry-stack_explorer
393
+ requirement: !ruby/object:Gem::Requirement
394
+ requirements:
395
+ - - ">="
396
+ - !ruby/object:Gem::Version
397
+ version: '0'
398
+ type: :development
399
+ prerelease: false
400
+ version_requirements: !ruby/object:Gem::Requirement
401
+ requirements:
402
+ - - ">="
403
+ - !ruby/object:Gem::Version
404
+ version: '0'
405
405
  - !ruby/object:Gem::Dependency
406
406
  name: devise-pwned_password
407
407
  requirement: !ruby/object:Gem::Requirement
@@ -563,6 +563,7 @@ files:
563
563
  - config/locales/en/billing/products.en.yml
564
564
  - config/locales/en/devise.en.yml
565
565
  - config/locales/en/doorkeeper.en.yml
566
+ - config/locales/en/framework_packages.yml
566
567
  - config/locales/en/invitations.en.yml
567
568
  - config/locales/en/memberships.en.yml
568
569
  - config/locales/en/oauth.en.yml
@@ -631,6 +632,7 @@ files:
631
632
  - docs/seeds.md
632
633
  - docs/super-scaffolding.md
633
634
  - docs/super-scaffolding/delegated-types.md
635
+ - docs/super-scaffolding/sortable.md
634
636
  - docs/teams.md
635
637
  - docs/testing.md
636
638
  - docs/themes.md