bullet_train 1.0.90 → 1.0.95
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/bullet-train.js +1 -1
- data/app/assets/javascripts/bullet-train.js.map +1 -1
- data/app/controllers/concerns/account/invitations/controller_base.rb +13 -15
- data/app/controllers/sessions_controller.rb +11 -0
- data/app/javascript/controllers/bulk_action_form_controller.js +1 -1
- data/app/javascript/controllers/bulk_actions_controller.js +1 -1
- data/app/mailers/concerns/mailers/base.rb +1 -0
- data/app/models/concerns/memberships/base.rb +5 -1
- data/app/models/concerns/users/base.rb +1 -2
- data/app/views/account/memberships/show.html.erb +3 -1
- data/app/views/account/onboarding/user_details/edit.html.erb +1 -1
- data/app/views/layouts/docs.html.erb +6 -0
- data/config/locales/en/framework_packages.yml +43 -0
- data/config/locales/en/invitations.en.yml +1 -0
- data/config/locales/en/teams.en.yml +1 -0
- data/docs/action-models.md +148 -0
- data/docs/field-partials/super-select.md +11 -0
- data/docs/index.md +1 -0
- data/docs/indirection.md +26 -1
- data/docs/onboarding.md +10 -10
- data/docs/super-scaffolding/sortable.md +137 -0
- data/docs/super-scaffolding.md +1 -0
- data/lib/bullet_train/resolver.rb +41 -32
- data/lib/bullet_train/version.rb +1 -1
- data/lib/bullet_train.rb +2 -2
- data/lib/tasks/bullet_train_tasks.rake +42 -75
- metadata +33 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40a00b0c6312326ea0ec3622729494460a0cfc90a51533daa823e55579c5a7dc
|
4
|
+
data.tar.gz: f6198fb630ec3d5f2b54614d34e6542e6f783286b94d2eab9ec300ec0df09223
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e7cd38b24aac43e77bf974bee30b2444c693539f91acf25a0f98832984e424ce7fb96f9ad88e057138eb80c41740ebfe88f088e33abe301af7d5352223a7587
|
7
|
+
data.tar.gz: 10c2473995e1e1eec0f57e82f9d9182df3d4cb3486b4b1298daefa80e439bb2f4cede431e41476bb8df1bf1db5e5715d8e7b21009dd4ff265320b8e0da4c204c
|
@@ -1,2 +1,2 @@
|
|
1
|
-
import{
|
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(/ <\/div>$/)?(e.element.innerHTML=e.element.innerHTML.slice(0,-12)+"</div>",this.removeTrailingWhitespace(e)):e.element.innerHTML.match(/ <\/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(/ <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/ <\\/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":"gDAqBA,SAASA,EAAwBC,GAC7B,MAAMC,GAAeD,EAAIE,MAAM,2CAA6C,IAAI,GAChF,GAAID,EACA,OAAOA,EAAYE,QAAQ,KAAM,KAAKA,QAAQ,MAAO,sBCtBhCC,EAa3BC,UACEC,KAAKC,qBAGPC,oBAAoBC,GAGlB,OAFAH,KAAKI,0BACLJ,KAAKK,4BAIPC,UAAUH,eACJA,YAAAA,EAAOI,SAAPC,EAAeC,MACjBT,KAAKU,SAAWP,EAAMI,OAAOE,IAC7BT,KAAKW,SAAWR,EAAMI,OAAOK,KAG/BZ,KAAKC,qBACLD,KAAKa,oBAGPZ,qBACED,KAAKc,QAAQC,UAAUC,OAAOhB,KAAKiB,YAAsC,IAAzBjB,KAAKU,SAASQ,QAGhEL,oBACE,IAAIM,EAAQnB,KAAKoB,iBACbpB,KAAKU,SAASQ,SAA4B,IAAlBlB,KAAKW,WAC/BQ,EAAQnB,KAAKqB,iBAAiBxB,QAAQ,QAASG,KAAKU,SAASQ,SAIxD,UADClB,KAAKsB,aAAaC,QACVvB,KAAKsB,aAAaE,MAAQL,EAC/BnB,KAAKsB,aAAaG,YAAcN,EAI7Cf,0BACEJ,KAAK0B,wBACL1B,KAAK2B,wBAGPD,wBACE1B,KAAK4B,sBAAsBC,QAAQC,IACjC9B,KAAKc,QAAQiB,YAAYD,KAI7BH,wBACE3B,KAAKU,SAASmB,QAAQG,IACpB,IAAIF,EAAQG,SAASC,cAAc,SACnCJ,EAAMK,KAAO,SACbL,EAAMM,KAAUpC,KAAKqC,oBAAmBrC,KAAKsC,wBAC7CR,EAAMN,MAAQQ,EACdhC,KAAKc,QAAQyB,YAAYT,KAI7BzB,yBACML,KAAKwC,wBACPxC,KAAKyC,qBAAqBjB,MAAQxB,KAAKW,SAAU,OAAQ,QAEzDX,KAAK0C,iBAITA,iBACE,IAAIZ,EAAQG,SAASC,cAAc,SACnCJ,EAAMK,KAAO,SACbL,EAAMM,KAAUpC,KAAKqC,oBAAmBrC,KAAK2C,sBAC7Cb,EAAMN,MAAQxB,KAAKW,SAAU,OAAQ,QACrCX,KAAKc,QAAQyB,YAAYT,MAlFpBc,QAAU,CAAE,SAAU,iBAAkB,oBACxCC,QAAU,CAAE,YACZC,OAAS,CACdC,YAAaC,OACbC,YAAaD,OACbvC,IAAKyC,MACLtC,IAAKuC,QACLC,WAAYJ,OACZK,aAAcL,OACdM,aAAcN,wBCVWlD,EAO3BC,UACEC,KAAKc,QAAQC,UAAUwC,IAAIvD,KAAKwD,0BAGlCC,mBACEzD,KAAK0D,iBAAmB1D,KAAK0D,gBAG/BC,oBACE3D,KAAK4D,gBACL5D,KAAK6D,0BAGPD,gBACE5D,KAAK8D,cAAcjC,QAAQkC,IACzBA,EAAaC,cAAc,IAAIC,YAAY,aAAc,CAAE1D,OAAQ,CACjEE,IAAKT,KAAKkE,YACVtD,IAAKZ,KAAKmE,kBAKhBC,gBAAgBjE,GACVH,KAAKmE,YACPnE,KAAKqE,aAELrE,KAAKsE,YAEPtE,KAAK6D,0BAGPS,YACEtE,KAAKuE,gBAAgB1C,QAAQ2C,IAC3BA,EAASC,SAAU,IAErBzE,KAAK4D,gBAGPS,aACErE,KAAKuE,gBAAgB1C,QAAQ2C,IAC3BA,EAASC,SAAU,IAErBzE,KAAK4D,gBAGPC,0BACE,IAAIW,EAAWxE,KAAK0E,wBAChBvD,EAAQnB,KAAK2E,qBAEb3E,KAAKmE,aACPK,EAASC,SAAU,EACnBD,EAASI,eAAgB,EACzBzD,EAAM6C,cAAc,IAAIC,YAAY,SAAU,CAAE1D,OAAQ,CAAEsE,cAAc,OAC/D7E,KAAKkE,YAAYhD,OAAS,GACnCsD,EAASI,eAAgB,EACzBzD,EAAM6C,cAAc,IAAIC,YAAY,SAAU,CAAE1D,OAAQ,CAAEsE,cAAc,QAExEL,EAASC,SAAU,EACnBD,EAASI,eAAgB,EACzBzD,EAAM6C,cAAc,IAAIC,YAAY,SAAU,CAAE1D,OAAQ,CAAEsE,cAAc,OAI5EC,yBACE9E,KAAKc,QAAQC,UAAUC,OAAOhB,KAAK+E,gBAAiB/E,KAAK0D,iBACzD1D,KAAKgF,oBAGPA,oBACEhF,KAAKiF,uBAAuBjB,cAAc,IAAIC,YAAY,SAAU,CAAE1D,OAAQ,CAAEsE,aAAc7E,KAAK0D,oBAGjGQ,kBACF,IAAIzD,EAAM,GAMV,OALAT,KAAKuE,gBAAgB1C,QAAQ2C,IACvBA,EAASC,SACXhE,EAAIyE,KAAKV,EAAShD,SAGff,EAGL0D,kBACF,YAAYD,YAAYhD,SAAWlB,KAAKuE,gBAAgBrD,UAzFnD0B,QAAU,CAAE,WAAY,oBAAqB,SAAU,mBAAoB,oBAC3EC,QAAU,CAAE,sBAAuB,gBACnCC,OAAS,CACdqC,WAAYhC,yBCJarD,EAG3BsF,OACEpF,KAAKqF,YAAY7D,MAAQxB,KAAKsF,aAAaC,UAC3CvF,KAAKqF,YAAYG,SACjBvD,SAASwD,YAAY,QACrBzF,KAAKsB,aAAaoE,UAAY,wEAC9BC,WAAW,WACT1D,SAAS2D,eAAe,UAAUF,UAAY,2DAC7C,SATE9C,QAAU,CAAC,SAAU,QAAS,0BCAV9C,yCA2B3B+F,uBAA0BC,IACpBA,EAAWhF,QAAQ4E,UAAU9F,MAAM,kBACrCkG,EAAWhF,QAAQ4E,UAAYI,EAAWhF,QAAQ4E,UAAUK,MAAM,GAAI,IAAM,SAC5E/F,KAAK6F,uBAAuBC,UAIhCE,yBAA4BF,IACtBA,EAAWhF,QAAQ4E,UAAU9F,MAAM,mBACrCkG,EAAWhF,QAAQ4E,UAAYI,EAAWhF,QAAQ4E,UAAUK,MAAM,GAAI,IAAM,SAC5E/F,KAAKgG,yBAAyBF,IACrBA,EAAWhF,QAAQ4E,UAAU9F,MAAM,qBAC5CkG,EAAWhF,QAAQ4E,UAAYI,EAAWhF,QAAQ4E,UAAUK,MAAM,GAAI,IAAM,SAC5E/F,KAAKgG,yBAAyBF,KArClCG,eAAeC,GACVA,EAAE3F,OAAO4F,SACVD,EAAEE,OAAOC,QAIbC,YACEtG,KAAKuG,iBAAiB1E,QAAQf,IAC5Bd,KAAK6F,uBAAuB/E,EAAQ0F,QACpCxG,KAAKgG,yBAAyBlF,EAAQ0F,QAGtC1F,EAAQ2F,cAAcC,cAAc,SAASlF,MAAQV,EAAQ4E,YAIjEiB,eAAeT,GACb,IAAIA,EAAEU,SAAWV,EAAEW,UAA0B,IAAbX,EAAEY,QAAe,CAC/CZ,EAAEa,iBACF,IAAIC,EAAOd,EAAEE,OAAOa,QAAQ,QAC5BjH,KAAKkH,WAAWF,IAqBpBE,WAAWF,GAGLA,EAAKG,cACPH,EAAKG,gBAELH,EAAKN,cAAc,iBAAiBU,WAjDjCxE,QAAU,CAAC,YAAa,0BCFJ9C,EAQ3BkB,SACE,MAAMqG,EAAYrH,KAAKsH,gBAAiBtH,KAAKuH,mBAAoBvH,KAAKwH,mBAClExH,KAAKsH,iBACPtH,KAAKyH,cAGPzH,KAAK0H,cAAc1D,cAAc,IAAIC,YAAYoD,IAG/CC,sBACF,YAAYI,cAAc3G,UAAU4G,SAAS3H,KAAKiB,aAGpDwG,cACEzH,KAAK0H,cAAc3G,UAAU6G,OAAO5H,KAAKiB,aAG3C4G,cACE7H,KAAK0H,cAAc3G,UAAUwC,IAAIvD,KAAKiB,gBAzBjC2B,QAAU,CAAE,aACZC,QAAU,CAAE,YACZC,OAAS,CACdgF,cAAe9E,OACf+E,cAAe/E,QCCNgF,MAAAA,EAAwB,CACnC,CAACC,EAA0B,kCAC3B,CAACC,EAAuB,8BACxB,CAACC,EAAqB,2BACtB,CAACC,EAAgB,sBACjB,CAACC,EAAsB,8BACvBC,IAAI,SAASC,GACb,MACMC,EAAaD,EAAE,GACrB,MAAO,CACLE,WAAYhJ,EAHF8I,EAAE,IAIZG,sBAAuBF,KCnB3BvG,SAAS0G,iBAAiB,aAAc,KAClCC,UAAUC,UAAUC,oBAAoBC,SAAS,aACnD9G,SAAS+G,KAAKjI,UAAUwC,IAAI"}
|
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(/ <\\/div>$/)) {\n trixEditor.element.innerHTML = trixEditor.element.innerHTML.slice(0, -12) + \"</div>\"\n this.removeTrailingWhitespace(trixEditor)\n } else if (trixEditor.element.innerHTML.match(/ <\\/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
|
-
|
61
|
-
|
62
|
-
|
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
|
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
|
@@ -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
|
-
|
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
|
-
|
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 %>
|
@@ -119,6 +119,12 @@
|
|
119
119
|
<% end %>
|
120
120
|
<% end %>
|
121
121
|
|
122
|
+
<%= render 'account/shared/menu/item', url: '/docs/action-models', label: 'Action Models' do |p| %>
|
123
|
+
<% p.content_for :icon do %>
|
124
|
+
<i class="fal fa-bars-progress ti ti-target"></i>
|
125
|
+
<% end %>
|
126
|
+
<% end %>
|
127
|
+
|
122
128
|
<%= render 'account/shared/menu/item', url: '/docs/seeds', label: 'Database Seeds' do |p| %>
|
123
129
|
<% p.content_for :icon do %>
|
124
130
|
<i class="fal fa-seedling ti ti-server"></i>
|
@@ -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:
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# Action Models
|
2
|
+
|
3
|
+
Action Models make it easy to scaffold and implement user-facing custom actions in your Bullet Train application and API in a RESTful way. Action Models are perfect for situations where you want one or more of the following:
|
4
|
+
|
5
|
+
- Bulk actions for a model where users can select one or more objects as targets.
|
6
|
+
- Long-running background tasks where you want to keep users updated on their progress in the UI or notify then after completion.
|
7
|
+
- Actions that have one or more configuration options available.
|
8
|
+
- Tasks that can be configured now but scheduled to take place later.
|
9
|
+
- Actions where you want to keep an in-app record of when it happened and who initiated the action.
|
10
|
+
- Tasks that one team member can initiate, but a team member with elevated privileges has to approve.
|
11
|
+
|
12
|
+
Examples of real-world features that can be easily implemented using Action Models include:
|
13
|
+
|
14
|
+
- A project manager can archive multiple projects at once.
|
15
|
+
- A customer service agent can refund multiple payments at once and provide a reason.
|
16
|
+
- A user can see progress updates while their 100,000 row CSV file is imported.
|
17
|
+
- A marketing manager can publish a blog post now or schedule it for publication tomorrow at 9 AM.
|
18
|
+
- A contributor can propose a content template change that will be applied after review and approval.
|
19
|
+
|
20
|
+
Importantly, Action Models aren't a special new layer in your application or a special section of your code base. Instead, they're just regular models, with corresponding scaffolded views and controllers, that exist alongside the rest of your domain model. They leverage Bullet Train's existing strengths around domain modeling and code generation with Super Scaffolding.
|
21
|
+
|
22
|
+
They're also super simple and very DRY. Consider the following example, assuming the process of archiving each project is very complicated and takes a lot of time:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class Projects::ArchiveAction < ApplicationRecord
|
26
|
+
include Actions::TargetsMany
|
27
|
+
include Actions::ProcessesAsync
|
28
|
+
include Actions::HasProgress
|
29
|
+
include Actions::CleansUp
|
30
|
+
|
31
|
+
belongs_to :team
|
32
|
+
|
33
|
+
def valid_targets
|
34
|
+
team.projects
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform_on_target(project)
|
38
|
+
project.archive
|
39
|
+
end
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Installation
|
44
|
+
|
45
|
+
### 1. Purchase Bullet Train Pro
|
46
|
+
|
47
|
+
First, [purchase Bullet Train Pro](https://buy.stripe.com/aEU7vc4dBfHtfO89AV). Once you've completed this process, you'll be issued a private token for the Bullet Train Pro package server. The process is currently completed manually, so you may have to way a little to receive your keys.
|
48
|
+
|
49
|
+
### 2. Install the Package
|
50
|
+
|
51
|
+
You'll need to specify both Ruby gems in your `Gemfile`, since we have to specify a private source for both:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
source "https://YOUR_TOKEN_HERE@gem.fury.io/bullettrain" do
|
55
|
+
gem "bullet_train-action_models"
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
Don't forget to run `bundle install` and `rails restart`.
|
60
|
+
|
61
|
+
## Super Scaffolding Commands
|
62
|
+
|
63
|
+
You can get detailed information about using Super Scaffolding to generate different types of Action Models like so:
|
64
|
+
|
65
|
+
```
|
66
|
+
bin/super-scaffold action-model:targets-many
|
67
|
+
bin/super-scaffold action-model:targets-one
|
68
|
+
bin/super-scaffold action-model:targets-one-parent
|
69
|
+
```
|
70
|
+
|
71
|
+
## Basic Example
|
72
|
+
|
73
|
+
### 1. Generate and scaffold an example `Project` model.
|
74
|
+
|
75
|
+
```
|
76
|
+
rails g model Project team:references name:string
|
77
|
+
bin/super-scaffold crud Project Team name:text_field
|
78
|
+
```
|
79
|
+
|
80
|
+
### 2. Generate and scaffold an archive action for projects.
|
81
|
+
|
82
|
+
```
|
83
|
+
bin/super-scaffold action-model:targets-many Archive Project Team
|
84
|
+
```
|
85
|
+
|
86
|
+
### 3. Implement the action logic.
|
87
|
+
|
88
|
+
Open `app/models/projects/archive_action.rb` and update the implementation of this method:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
def perform_on_target(project)
|
92
|
+
project.archive
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
You're done!
|
97
|
+
|
98
|
+
## Additional Examples
|
99
|
+
|
100
|
+
### Add configuration options to an action.
|
101
|
+
|
102
|
+
Because Action Models are just regular models, you can add new fields to them with Super Scaffolding the same as any other model. This is an incredible strength, because it means the configuration options for your Action Models can leverage the entire suite of form field types available in Bullet Train, and maintaining the presentation of those options to users is like maintaining any other model form in your application.
|
103
|
+
|
104
|
+
For example:
|
105
|
+
|
106
|
+
```
|
107
|
+
rails g migration add notify_users_to_projects_archive_actions notify_users:boolean
|
108
|
+
# side quest: update the generated migration with `default: false` on the new boolean field.
|
109
|
+
bin/super-scaffold crud-field Projects::ArchiveAction notify_users:boolean
|
110
|
+
```
|
111
|
+
|
112
|
+
Now users will be prompted with that option when they perform this action, and you can update your logic to take action based on it, or at least pass on the information to another method that takes action based on it:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
def perform_on_target(project)
|
116
|
+
project.archive(send_notification: notify_users)
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
## Action Types
|
121
|
+
|
122
|
+
Action Models can be generated in three flavors:
|
123
|
+
|
124
|
+
- `action-model:targets-many`
|
125
|
+
- `action-model:targets-one`
|
126
|
+
- `action-model:targets-one-parent`
|
127
|
+
|
128
|
+
### Targets Many
|
129
|
+
|
130
|
+
Action Models that _can_ target many objects are by far the most common, but it's important to understand that they're not only presented to users as bulk actions. Instead, by default, they're presented to users as an action available for each individual object, but they're also presented as a bulk action when users have selected multiple objects.
|
131
|
+
|
132
|
+
"Targets many" actions live at the same level in the domain model (and belong to the same parent) as the model they target. (If this doesn't make immediate sense, just consider that it would be impossible for instances of these actions to live under multiple targets at the same time.)
|
133
|
+
|
134
|
+
### Targets One
|
135
|
+
|
136
|
+
Sometimes you have an action that will only ever target one object at a time. In this case, they're generated a little differently and live under (and belong to) the model they target.
|
137
|
+
|
138
|
+
When deciding between "targets many" and "targets one", our recommendation is that you only use "targets one" for actions that you know for certain could never make sense targeting more than one object at the same time. For example, if you're creating a send action for an email that includes configuration options and scheduling details, you may be reasonably confident that you never need users to be able to schedule two different emails at the same time with the same settings. That would be a good candidate for a "targets one" action.
|
139
|
+
|
140
|
+
### Targets One Parent
|
141
|
+
|
142
|
+
This final type of action is available for things like importers that don't necessarily target specific existing objects by ID, but instead create many new or affect many existing objects under a specific parent based on the configuration of the action (like an attached CSV file.) These objects don't "target many" per se, but they live at the same level (and belong to the same parent) as the models they end up creating or affecting.
|
143
|
+
|
144
|
+
## Frequently Asked Questions
|
145
|
+
|
146
|
+
### Do Action Models have to be persisted to the database?
|
147
|
+
|
148
|
+
No. Action Models extend from `ApplicationRecord` by default, but if you're not using features that depend on persistence to the database, you can make them `include ActiveModel::API` instead. That said, it's probably not worth the trouble. As an alternative, consider just including `Actions::CleansUp` in your action to ensure it removes itself from the database after completion.
|
@@ -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/index.md
CHANGED
@@ -16,6 +16,7 @@
|
|
16
16
|
|
17
17
|
## Developer Tools
|
18
18
|
- [Super Scaffolding](/docs/super-scaffolding.md)
|
19
|
+
- [Action Models](/docs/action-models.md)
|
19
20
|
- [Database Seeds](/docs/seeds.md)
|
20
21
|
- [Test Suite](/docs/testing.md)
|
21
22
|
- [Point-and-Click Test Writing](https://github.com/bullet-train-co/magic_test) <i class="ti ti-new-window ml-2"></i>
|
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
|
-
|
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
|
-
|
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/
|
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
|
+
```
|
data/docs/super-scaffolding.md
CHANGED
@@ -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
|
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?("
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/bullet_train/version.rb
CHANGED
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
|
-
|
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 "
|
165
|
-
puts "
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
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
|
-
|
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
|
-
|
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.
|
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-
|
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
|
@@ -607,6 +608,7 @@ files:
|
|
607
608
|
- db/migrate/20210816072508_add_locale_to_teams.rb
|
608
609
|
- db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb
|
609
610
|
- db/migrate/20211027002944_add_doorkeeper_application_to_users.rb
|
611
|
+
- docs/action-models.md
|
610
612
|
- docs/api.md
|
611
613
|
- docs/authentication.md
|
612
614
|
- docs/billing/stripe.md
|
@@ -630,6 +632,7 @@ files:
|
|
630
632
|
- docs/seeds.md
|
631
633
|
- docs/super-scaffolding.md
|
632
634
|
- docs/super-scaffolding/delegated-types.md
|
635
|
+
- docs/super-scaffolding/sortable.md
|
633
636
|
- docs/teams.md
|
634
637
|
- docs/testing.md
|
635
638
|
- docs/themes.md
|