diff --git a/package-lock.json b/package-lock.json index 1f46222..e004f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "concept7-statamic-mailerlite", + "name": "statamic-mailerlite", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/resources/dist/build/assets/addon-e4Zwj-7O.js b/resources/dist/build/assets/addon-e4Zwj-7O.js deleted file mode 100644 index 057fa66..0000000 --- a/resources/dist/build/assets/addon-e4Zwj-7O.js +++ /dev/null @@ -1 +0,0 @@ -const S=window.Vue,{BaseTransition:M,BaseTransitionPropsValidators:E,Comment:I,DeprecationTypes:D,EffectScope:A,ErrorCodes:L,ErrorTypeStrings:H,Fragment:F,KeepAlive:B,ReactiveEffect:V,Static:N,Suspense:z,Teleport:G,Text:O,TrackOpTypes:U,Transition:q,TransitionGroup:K,TriggerOpTypes:W,VueElement:j,__esModule:$,assertNumber:Q,callWithAsyncErrorHandling:Y,callWithErrorHandling:J,camelize:X,capitalize:Z,cloneVNode:ee,compatUtils:te,compile:oe,computed:T,createApp:re,createBlock:l,createCommentVNode:ne,createElementBlock:u,createElementVNode:ie,createHydrationRenderer:ae,createPropsRestProxy:se,createRenderer:le,createSSRApp:de,createSlots:ce,createStaticVNode:ue,createTextVNode:d,createVNode:i,customRef:pe,defineAsyncComponent:me,defineComponent:ge,defineCustomElement:Ce,defineEmits:he,defineExpose:fe,defineModel:Se,defineOptions:Te,defineProps:ye,defineSSRCustomElement:we,defineSlots:Pe,devtools:ve,effect:be,effectScope:Re,getCurrentInstance:xe,getCurrentScope:ke,getCurrentWatcher:_e,getTransitionRawChildren:Me,guardReactiveProps:Ee,h:Ie,handleError:De,hasInjectionContext:Ae,hydrate:Le,hydrateOnIdle:He,hydrateOnInteraction:Fe,hydrateOnMediaQuery:Be,hydrateOnVisible:Ve,initCustomFormatter:Ne,initDirectivesForSSR:ze,inject:Ge,isMemoSame:Oe,isProxy:Ue,isReactive:qe,isReadonly:Ke,isRef:We,isRuntimeOnly:je,isShallow:$e,isVNode:Qe,markRaw:Ye,mergeDefaults:Je,mergeModels:Xe,mergeProps:Ze,nextTick:et,nodeOps:tt,normalizeClass:ot,normalizeProps:rt,normalizeStyle:nt,onActivated:it,onBeforeMount:at,onBeforeUnmount:st,onBeforeUpdate:lt,onDeactivated:dt,onErrorCaptured:ct,onMounted:ut,onRenderTracked:pt,onRenderTriggered:mt,onScopeDispose:gt,onServerPrefetch:Ct,onUnmounted:ht,onUpdated:ft,onWatcherCleanup:St,openBlock:a,patchProp:Tt,popScopeId:yt,provide:wt,proxyRefs:Pt,pushScopeId:vt,queuePostFlushCb:bt,reactive:Rt,readonly:xt,ref:kt,registerRuntimeCompiler:_t,render:Mt,renderList:Et,renderSlot:It,resolveComponent:Dt,resolveDirective:At,resolveDynamicComponent:Lt,resolveFilter:Ht,resolveTransitionHooks:Ft,setBlockTracking:Bt,setDevtoolsHook:Vt,setTransitionHooks:Nt,shallowReactive:zt,shallowReadonly:Gt,shallowRef:Ot,ssrContextKey:Ut,ssrUtils:qt,stop:Kt,toDisplayString:s,toHandlerKey:Wt,toHandlers:jt,toRaw:$t,toRef:Qt,toRefs:Yt,toValue:Jt,transformVNodeArgs:Xt,triggerRef:Zt,unref:t,useAttrs:eo,useCssModule:to,useCssVars:oo,useHost:ro,useId:no,useModel:io,useSSRContext:ao,useShadowRoot:so,useSlots:lo,useTemplateRef:co,useTransitionState:uo,vModelCheckbox:po,vModelDynamic:mo,vModelRadio:go,vModelSelect:Co,vModelText:ho,vShow:fo,version:So,warn:To,watch:yo,watchEffect:wo,watchPostEffect:Po,watchSyncEffect:vo,withAsyncContext:bo,withCtx:r,withDefaults:Ro,withDirectives:xo,withKeys:ko,withMemo:_o,withModifiers:Mo,withScopeId:Eo}=S,{Alert:Io,AuthCard:Do,Avatar:Ao,Badge:p,Button:y,ButtonGroup:Lo,Calendar:Ho,Card:Fo,CardList:Bo,CardListItem:Vo,CardPanel:No,CharacterCounter:zo,Checkbox:Go,CheckboxGroup:Oo,CodeEditor:Uo,Combobox:qo,CommandPaletteItem:Ko,ConfirmationModal:Wo,Context:jo,ContextFooter:$o,ContextHeader:Qo,ContextItem:Yo,ContextLabel:Jo,ContextMenu:Xo,ContextSeparator:Zo,CreateForm:er,DatePicker:tr,DateRangePicker:or,Description:rr,DocsCallout:nr,DragHandle:ir,Dropdown:ar,DropdownItem:m,DropdownLabel:sr,DropdownMenu:lr,DropdownSeparator:dr,DropdownFooter:cr,DropdownHeader:ur,Editable:pr,ErrorMessage:mr,EmptyStateItem:gr,EmptyStateMenu:Cr,Field:hr,Header:w,Heading:fr,HoverCard:Sr,Icon:Tr,Input:yr,InputGroup:wr,InputGroupAppend:Pr,InputGroupPrepend:vr,Label:br,Listing:P,ListingCustomizeColumns:Rr,ListingFilters:xr,ListingHeaderCell:kr,ListingPagination:_r,ListingPresets:Mr,ListingPresetTrigger:Er,ListingRowActions:Ir,ListingSearch:Dr,ListingTable:Ar,ListingTableBody:Lr,ListingTableHead:Hr,ListingToggleAll:Fr,LivePreview:Br,LivePreviewPopout:Vr,MiddleEllipsis:Nr,Modal:zr,ModalClose:Gr,ModalTitle:Or,Pagination:Ur,Panel:qr,PanelFooter:Kr,PanelHeader:Wr,Popover:jr,PublishComponents:$r,PublishContainer:Qr,publishContextKey:Yr,injectPublishContext:Jr,PublishField:Xr,PublishFields:Zr,PublishFieldsProvider:en,PublishForm:tn,PublishLocalizations:on,PublishSections:rn,PublishTabs:nn,Radio:an,RadioGroup:sn,Select:ln,Separator:dn,Slider:cn,Skeleton:un,SplitterGroup:pn,SplitterPanel:mn,SplitterResizeHandle:gn,StatusIndicator:Cn,Subheading:hn,Switch:fn,TabContent:Sn,Stack:Tn,StackClose:yn,StackHeader:wn,StackFooter:Pn,StackContent:vn,Table:bn,TableCell:Rn,TableColumn:xn,TableColumns:kn,TableRow:_n,TableRows:Mn,TabList:En,TabProvider:In,Tabs:Dn,TabTrigger:An,Text:Ln,Textarea:Hn,TimePicker:Fn,ToggleGroup:Bn,ToggleItem:Vn,Widget:Nn,registerIconSet:zn,registerIconSetFromStrings:Gn}=__STATAMIC__.ui,{Form:On,Head:Un,Link:v,router:g,toggleArchitecturalBackground:qn,useArchitecturalBackground:Kn,useForm:Wn,usePoll:jn}=__STATAMIC__.inertia,b={class:"max-w-page mx-auto"},R={key:0,class:"card p-6 text-center text-gray-500"},x={__name:"Index",props:{configs:{type:Array,required:!0},initialColumns:{type:Array,required:!0},title:{type:String,required:!0},createUrl:{type:String,required:!0}},setup(n){const C=n,h=T(()=>C.configs.length===0),c=()=>g.reload();function f(e){confirm(__("Are you sure you want to delete this form config?"))&&g.delete(e.delete_url,{onSuccess:()=>c()})}return(e,k)=>(a(),u("div",b,[i(t(w),{title:n.title,icon:"email"},{default:r(()=>[i(t(y),{href:n.createUrl,text:e.__("Create"),variant:"primary"},null,8,["href","text"])]),_:1},8,["title"]),h.value?(a(),u("div",R,s(e.__("No form configs found. Create one to get started.")),1)):(a(),l(t(P),{key:1,items:n.configs,columns:n.initialColumns,"allow-search":!1,"allow-customizing-columns":!1,onRefreshing:c},{"cell-title":r(({row:o})=>[i(t(v),{href:o.edit_url},{default:r(()=>[d(s(o.title),1)]),_:2},1032,["href"])]),"cell-enabled":r(({row:o})=>[o.enabled?(a(),l(t(p),{key:0,color:"green"},{default:r(()=>[d(s(e.__("Yes")),1)]),_:1})):(a(),l(t(p),{key:1,color:"gray"},{default:r(()=>[d(s(e.__("No")),1)]),_:1}))]),"prepended-row-actions":r(({row:o})=>[i(t(m),{text:e.__("Edit"),icon:"cog",href:o.edit_url},null,8,["text","href"]),i(t(m),{text:e.__("Delete"),icon:"trash",variant:"destructive",onClick:_=>f(o)},null,8,["text","onClick"])]),_:1},8,["items","columns"]))]))}};Statamic.booting(()=>{Statamic.$inertia.register("statamic-mailerlite::FormConfig/Index",x)}); diff --git a/resources/dist/build/assets/addon-l0sNRNKZ.js b/resources/dist/build/assets/addon-l0sNRNKZ.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/resources/dist/build/assets/addon-l0sNRNKZ.js @@ -0,0 +1 @@ + diff --git a/resources/dist/build/manifest.json b/resources/dist/build/manifest.json index 25faecd..3de1dd2 100644 --- a/resources/dist/build/manifest.json +++ b/resources/dist/build/manifest.json @@ -1,6 +1,6 @@ { "resources/js/addon.js": { - "file": "assets/addon-e4Zwj-7O.js", + "file": "assets/addon-l0sNRNKZ.js", "name": "addon", "src": "resources/js/addon.js", "isEntry": true diff --git a/resources/js/addon.js b/resources/js/addon.js index c760283..459b729 100644 --- a/resources/js/addon.js +++ b/resources/js/addon.js @@ -1,5 +1,3 @@ -import Index from './pages/FormConfig/Index.vue'; - -Statamic.booting(() => { - Statamic.$inertia.register('statamic-mailerlite::FormConfig/Index', Index); -}); +// No custom Control Panel components. MailerLite configuration lives on the +// native form configure page, injected via Form::appendConfigFields() in the +// addon service provider. diff --git a/resources/js/pages/FormConfig/Index.vue b/resources/js/pages/FormConfig/Index.vue deleted file mode 100644 index 2c3c529..0000000 --- a/resources/js/pages/FormConfig/Index.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - {{ __('No form configs found. Create one to get started.') }} - - - - - {{ config.title }} - - - {{ __('Yes') }} - {{ __('No') }} - - - - - - - - diff --git a/routes/cp.php b/routes/cp.php deleted file mode 100644 index 6d32849..0000000 --- a/routes/cp.php +++ /dev/null @@ -1,13 +0,0 @@ -name('statamic-mailerlite.')->group(function () { - Route::get('form-config', [FormConfigController::class, 'index'])->name('form-config.index'); - Route::get('form-config/create', [FormConfigController::class, 'create'])->name('form-config.create'); - Route::post('form-config', [FormConfigController::class, 'store'])->name('form-config.store'); - Route::get('form-config/{id}', [FormConfigController::class, 'edit'])->name('form-config.edit'); - Route::patch('form-config/{id}', [FormConfigController::class, 'update'])->name('form-config.update'); - Route::delete('form-config/{id}', [FormConfigController::class, 'destroy'])->name('form-config.destroy'); -}); diff --git a/src/Fieldtypes/MailerLiteFields.php b/src/Fieldtypes/MailerLiteFields.php new file mode 100644 index 0000000..42fe375 --- /dev/null +++ b/src/Fieldtypes/MailerLiteFields.php @@ -0,0 +1,100 @@ +> */ + private static array $cache = []; + + protected function toItemArray($id) + { + return collect($this->fields())->firstWhere('id', $id) ?? $this->invalidItemArray($id); + } + + public function getIndexItems($request) + { + return collect($this->fields()); + } + + protected function getColumns() + { + return [ + Column::make('title')->label(__('Name')), + ]; + } + + public function preProcessIndex($data) + { + if (! $data) { + return []; + } + + return collect($data)->map(function ($id) { + $item = $this->toItemArray($id); + + return $item['title'] ?? $id; + })->join(', '); + } + + /** + * The selectable MailerLite subscriber fields, always including the built-in + * email attribute. Values are MailerLite field keys (the subscriber payload + * is keyed by key, not id); email maps to the SDK's top-level email attribute. + * + * @return array + */ + private function fields(): array + { + $baseline = [ + ['id' => 'email', 'title' => __('Email (built-in)')], + ]; + + $apiKey = Addon::get('concept7/statamic-mailerlite')->setting('api_key'); + + if (blank($apiKey)) { + return $baseline; + } + + $cacheKey = hash('sha256', $apiKey); + + if (isset(self::$cache[$cacheKey])) { + return self::$cache[$cacheKey]; + } + + $custom = rescue(function () use ($apiKey): array { + $response = (new MailerLite(['api_key' => $apiKey])) + ->fields + ->get([ + 'limit' => 100, + 'sort' => 'name', + ]); + + return collect($response['body']['data'] ?? []) + ->map(fn (array $field): array => [ + 'id' => $field['key'], + 'title' => $field['name'], + ]) + ->all(); + }, [], report: false); + + return self::$cache[$cacheKey] = [...$baseline, ...$custom]; + } +} diff --git a/src/Http/Controllers/CP/FormConfigController.php b/src/Http/Controllers/CP/FormConfigController.php deleted file mode 100644 index 776a3fe..0000000 --- a/src/Http/Controllers/CP/FormConfigController.php +++ /dev/null @@ -1,256 +0,0 @@ -label(__('Form')), - Column::make('enabled')->label(__('Enabled')), - Column::make('subscriber_groups')->label(__('Subscriber Groups')), - ]; - - $configs = $this->repository->all()->map(function ($config) { - $form = Form::find($config->form()); - - return [ - 'id' => $config->id(), - 'title' => $form->title(), - 'enabled' => $config->enabled(), - 'subscriber_groups' => count($config->subscriberGroups()) ?: '—', - 'edit_url' => cp_route('statamic-mailerlite.form-config.edit', $config->id()), - 'delete_url' => cp_route('statamic-mailerlite.form-config.destroy', $config->id()), - ]; - })->values(); - - return Inertia::render('statamic-mailerlite::FormConfig/Index', [ - 'configs' => $configs, - 'initialColumns' => $columns, - 'title' => __('MailerLite'), - 'createUrl' => cp_route('statamic-mailerlite.form-config.create'), - ]); - } - - public function create() - { - return PublishForm::make($this->formBlueprint()) - ->title(__('Create Form Config')) - ->values([ - 'form_handle' => '', - 'enabled' => false, - 'subscriber_groups' => [], - 'field_mapping' => [], - ]) - ->asConfig() - ->submittingTo(cp_route('statamic-mailerlite.form-config.store'), 'POST'); - } - - public function store(Request $request) - { - $publishForm = PublishForm::make($this->formBlueprint()); - $values = $publishForm->submit($request->all()); - - $formHandle = $values['form_handle']; - - $this->findFormOrFail($formHandle); - - if ($this->repository->findByForm($formHandle)) { - abort(422, __('A config for this form already exists.')); - } - - $id = Str::uuid()->toString(); - - $config = $this->repository->make(); - $config - ->id($id) - ->form($formHandle) - ->enabled($values['enabled']) - ->subscriberGroups($values['subscriber_groups'] ?: null) - ->fieldMapping($values['field_mapping'] ?? []); - - $this->repository->save($config); - - session()->flash('success', __('Form config created')); - - return ['redirect' => cp_route('statamic-mailerlite.form-config.edit', $id)]; - } - - public function edit(string $id) - { - $config = $this->repository->find($id); - - if (! $config) { - abort(404, __('Form config not found.')); - } - - $form = $this->findFormOrFail($config->form()); - - $values = [ - 'enabled' => $config->enabled(), - 'subscriber_groups' => $config->subscriberGroups(), - 'field_mapping' => $config->fieldMapping(), - ]; - - return PublishForm::make($this->formBlueprint($config->form())) - ->title(__('MailerLite — :form', ['form' => $form->title()])) - ->values($values) - ->asConfig() - ->submittingTo(cp_route('statamic-mailerlite.form-config.update', $id)); - } - - public function update(string $id, Request $request) - { - $config = $this->repository->find($id); - - if (! $config) { - abort(404, __('Form config not found.')); - } - - $this->findFormOrFail($config->form()); - - $publishForm = PublishForm::make($this->formBlueprint($config->form())); - $values = $publishForm->submit($request->all()); - - $config - ->enabled($values['enabled']) - ->subscriberGroups($values['subscriber_groups'] ?: null) - ->fieldMapping($values['field_mapping'] ?? []); - - $this->repository->save($config); - - $this->success(__('Saved')); - } - - public function destroy(string $id) - { - $config = $this->repository->find($id); - - if (! $config) { - abort(404, __('Form config not found.')); - } - - $this->repository->delete($config); - - return [ - 'message' => __('Form config deleted'), - 'redirect' => cp_route('statamic-mailerlite.form-config.index'), - ]; - } - - private function findFormOrFail(string $handle): \Statamic\Contracts\Forms\Form - { - return Form::findOrFail($handle); - } - - private function formBlueprint(?string $formHandle = null): \Statamic\Fields\Blueprint - { - $sections = []; - - if (! $formHandle) { - $configuredForms = $this->repository->all() - ->map(fn ($config) => $config->form()) - ->all(); - - $availableForms = Form::all() - ->reject(fn ($form) => in_array($form->handle(), $configuredForms)) - ->mapWithKeys(fn ($form) => [$form->handle() => __($form->title())]) - ->all(); - - $sections[] = [ - 'display' => __('Form'), - 'fields' => [ - [ - 'handle' => 'form_handle', - 'field' => [ - 'type' => 'select', - 'display' => __('Statamic Form'), - 'instructions' => __('Select the form to configure for MailerLite.'), - 'options' => $availableForms, - 'validate' => 'required', - ], - ], - ], - ]; - } - - $sections[] = [ - 'display' => __('General'), - 'fields' => [ - [ - 'handle' => 'enabled', - 'field' => [ - 'type' => 'toggle', - 'display' => __('Enabled'), - 'instructions' => __('Enable MailerLite integration for this form.'), - ], - ], - [ - 'handle' => 'subscriber_groups', - 'field' => [ - 'type' => 'mailer_lite_groups', - 'display' => __('Subscriber Groups'), - 'instructions' => __('Select the MailerLite subscriber groups to add subscribers to.'), - ], - ], - ], - ]; - - $sections[] = [ - 'display' => __('Field Mapping'), - 'fields' => [ - [ - 'handle' => 'field_mapping', - 'field' => [ - 'type' => 'grid', - 'display' => __('Field Mapping'), - 'instructions' => __('Map form fields to MailerLite subscriber fields.'), - 'add_row' => __('Add Mapping'), - 'fields' => [ - [ - 'handle' => 'form_field', - 'field' => [ - 'type' => 'text', - 'display' => __('Form Field'), - ], - ], - [ - 'handle' => 'mailerlite_field', - 'field' => [ - 'type' => 'text', - 'display' => __('MailerLite Field'), - ], - ], - ], - ], - ], - ], - ]; - - return Blueprint::make()->setContents([ - 'tabs' => [ - 'main' => [ - 'sections' => $sections, - ], - ], - ]); - } -} diff --git a/src/Listeners/SubmissionCreatedListener.php b/src/Listeners/SubmissionCreatedListener.php index 5e95321..d278a76 100644 --- a/src/Listeners/SubmissionCreatedListener.php +++ b/src/Listeners/SubmissionCreatedListener.php @@ -3,36 +3,39 @@ namespace Concept7\StatamicMailerLite\Listeners; use Concept7\StatamicMailerLite\Jobs\CreateSubscriberJob; -use Concept7\StatamicMailerLite\Stache\FormConfigRepository; use Statamic\Events\SubmissionCreated; use Statamic\Facades\Addon; +use Statamic\Support\Arr; class SubmissionCreatedListener { - public function __construct( - private FormConfigRepository $repository, - ) {} - public function handle(SubmissionCreated $event): void { - $formHandle = $event->submission->form()->handle(); - $config = $this->repository->findByForm($formHandle); + $form = $event->submission->form(); + + if ($form->get('mailerlite_enabled') !== true) { + return; + } + + $fieldMapping = $form->get('mailerlite_field_mapping') ?: []; - if (! $config || ! $config->enabled()) { + if (! is_array($fieldMapping) || $fieldMapping === []) { return; } $apiKey = Addon::get('concept7/statamic-mailerlite')->setting('api_key'); - if (! $apiKey) { + if (blank($apiKey)) { return; } - CreateSubscriberJob::dispatch( + $group = $form->get('mailerlite_group'); + + dispatch(new CreateSubscriberJob( submissionData: $event->submission->data()->all(), - fieldMapping: $config->fieldMapping(), - subscriberGroups: $config->subscriberGroups(), + fieldMapping: array_values($fieldMapping), + subscriberGroups: filled($group) ? Arr::wrap($group) : [], apiKey: $apiKey, - ); + )); } } diff --git a/src/Stache/FormConfigEntry.php b/src/Stache/FormConfigEntry.php deleted file mode 100644 index 655e588..0000000 --- a/src/Stache/FormConfigEntry.php +++ /dev/null @@ -1,81 +0,0 @@ -fluentlyGetOrSet('slug')->args(func_get_args()); - } - - public function form(?string $form = null) - { - if (func_num_args() === 0) { - return $this->get('form'); - } - - return $this->set('form', $form); - } - - public function path(): string - { - return $this->initialPath ?? $this->buildPath(); - } - - public function buildPath(): string - { - return base_path("content/mailerlite/{$this->id()}.yaml"); - } - - /** @return array{form: string, enabled: bool, subscriber_groups: array|null, field_mapping: array} */ - public function fileData(): array - { - return Arr::removeNullValues([ - 'form' => $this->form(), - 'enabled' => $this->enabled() ?: null, - 'subscriber_groups' => $this->subscriberGroups() ?: null, - 'field_mapping' => $this->fieldMapping() ?: null, - ]); - } - - public function enabled(?bool $enabled = null) - { - if (func_num_args() === 0) { - return (bool) $this->get('enabled', false); - } - - return $this->set('enabled', $enabled); - } - - /** @param array|null $subscriberGroups */ - public function subscriberGroups(?array $subscriberGroups = null) - { - if (func_num_args() === 0) { - return $this->get('subscriber_groups', []); - } - - return $this->set('subscriber_groups', $subscriberGroups); - } - - /** - * @param array|null $fieldMapping - * @return array|self - */ - public function fieldMapping(?array $fieldMapping = null) - { - if (func_num_args() === 0) { - return $this->get('field_mapping', []); - } - - return $this->set('field_mapping', $fieldMapping); - } - - public function fileExtension(): string - { - return 'yaml'; - } -} diff --git a/src/Stache/FormConfigRepository.php b/src/Stache/FormConfigRepository.php deleted file mode 100644 index 24372ae..0000000 --- a/src/Stache/FormConfigRepository.php +++ /dev/null @@ -1,58 +0,0 @@ -stache = $stache; - $this->store = $stache->store('mailerlite-form-configs'); - } - - public function all(): EntryCollection - { - return EntryCollection::make( - $this->store->getItems($this->store->paths()->keys()->all()) - ); - } - - /** @param string $id */ - public function find($id): ?FormConfigEntry - { - return $this->store->getItem($id); - } - - public function findByForm(string $formHandle): ?FormConfigEntry - { - return $this->all()->first(fn (FormConfigEntry $config) => $config->form() === $formHandle); - } - - public function make(): FormConfigEntry - { - return new FormConfigEntry; - } - - public function save($entry): void - { - $this->store->save($entry); - } - - public function delete($entry): void - { - $this->store->delete($entry); - } - - /** @return array */ - public static function bindings(): array - { - return [ - Entry::class => FormConfigEntry::class, - ]; - } -} diff --git a/src/Stache/FormConfigStore.php b/src/Stache/FormConfigStore.php deleted file mode 100644 index faab5c7..0000000 --- a/src/Stache/FormConfigStore.php +++ /dev/null @@ -1,59 +0,0 @@ -getPathName()), $this->directory); - - return substr_count($filename, '/') === 0 && $file->getExtension() === 'yaml'; - } - - public function makeItemFromFile($path, $contents) - { - $relative = Str::after($path, $this->directory); - $id = Str::before($relative, '.yaml'); - - $data = YAML::file($path)->parse($contents); - - $entry = (new FormConfigEntry) - ->id($id) - ->form($data['form'] ?? null) - ->enabled($data['enabled'] ?? false) - ->fieldMapping($data['field_mapping'] ?? []) - ->initialPath($path); - - if (isset($data['subscriber_groups'])) { - $entry->subscriberGroups($data['subscriber_groups']); - } - - return $entry; - } - - public function getItemKey($item): string - { - return $item->id(); - } - - protected function getKeyFromPath($path) - { - if ($key = parent::getKeyFromPath($path)) { - return $key; - } - - return pathinfo($path, PATHINFO_FILENAME); - } -} diff --git a/src/StatamicMailerliteServiceProvider.php b/src/StatamicMailerliteServiceProvider.php index cad6573..03b0de8 100644 --- a/src/StatamicMailerliteServiceProvider.php +++ b/src/StatamicMailerliteServiceProvider.php @@ -3,9 +3,10 @@ namespace Concept7\StatamicMailerLite; use Concept7\StatamicMailerLite\Listeners\SubmissionCreatedListener; -use Concept7\StatamicMailerLite\Stache\FormConfigStore; +use Statamic\Contracts\Forms\Form as FormContract; use Statamic\Events\SubmissionCreated; -use Statamic\Facades\CP\Nav; +use Statamic\Facades\Form; +use Statamic\Fields\Field; use Statamic\Providers\AddonServiceProvider; class StatamicMailerLiteServiceProvider extends AddonServiceProvider @@ -21,24 +22,71 @@ class StatamicMailerLiteServiceProvider extends AddonServiceProvider 'publicDirectory' => 'resources/dist', ]; - public function register(): void + public function bootAddon(): void { - parent::register(); - - $this->app->booted(function () { - $this->app['stache']->registerStore( - (new FormConfigStore)->directory(base_path('content/mailerlite')) - ); - }); + // bootAddon() already runs inside Statamic's booted phase, so the form + // repository is available and config fields can be appended directly. + // Wrapping this in Statamic::booted() would queue a callback that never + // runs — runBootedCallbacks() is already mid-drain at this point. + Form::all()->each(fn (FormContract $form) => Form::appendConfigFields( + $form->handle(), + 'MailerLite', + static::configFields($form), + )); } - public function bootAddon(): void + /** + * Build the MailerLite section appended to a form's native configure page. + * + * @return array> + */ + public static function configFields(FormContract $form): array { - Nav::extend(function () { - Nav::create('MailerLite') - ->section('Tools') - ->route('statamic-mailerlite.form-config.index') - ->icon('mail'); - }); + $formFieldOptions = $form->blueprint()->fields()->all() + ->mapWithKeys(fn (Field $field): array => [$field->handle() => $field->display()]) + ->all(); + + return [ + 'mailerlite_enabled' => [ + 'type' => 'toggle', + 'display' => __('Enable MailerLite sync'), + 'instructions' => __('When on, submissions to this form are synced to MailerLite as subscribers.'), + 'default' => false, + ], + 'mailerlite_group' => [ + 'type' => 'mailer_lite_groups', + 'display' => __('Group'), + 'instructions' => __('Subscribers will be added to this MailerLite group.'), + 'mode' => 'select', + 'max_items' => 1, + ], + 'mailerlite_field_mapping' => [ + 'type' => 'grid', + 'mode' => 'table', + 'display' => __('Field mapping'), + 'instructions' => __('Map each form field to a MailerLite subscriber field.'), + 'add_row' => __('Add mapping'), + 'fields' => [ + [ + 'handle' => 'form_field', + 'field' => [ + 'type' => 'select', + 'display' => __('Form field'), + 'options' => $formFieldOptions, + 'clearable' => false, + ], + ], + [ + 'handle' => 'mailerlite_field', + 'field' => [ + 'type' => 'mailer_lite_fields', + 'display' => __('MailerLite field'), + 'mode' => 'select', + 'max_items' => 1, + ], + ], + ], + ], + ]; } } diff --git a/tests/Feature/MailerLiteConfigFieldsTest.php b/tests/Feature/MailerLiteConfigFieldsTest.php new file mode 100644 index 0000000..040817f --- /dev/null +++ b/tests/Feature/MailerLiteConfigFieldsTest.php @@ -0,0 +1,43 @@ +toHaveKeys(['mailerlite_enabled', 'mailerlite_group', 'mailerlite_field_mapping']); + expect($fields['mailerlite_enabled']['type'])->toBe('toggle'); + expect($fields['mailerlite_enabled']['default'])->toBeFalse(); + expect($fields['mailerlite_group']['type'])->toBe('mailer_lite_groups'); + expect($fields['mailerlite_group']['max_items'])->toBe(1); + expect($fields['mailerlite_field_mapping']['type'])->toBe('grid'); +}); + +it('uses a single-select MailerLite field inside the mapping grid', function () { + $fields = StatamicMailerLiteServiceProvider::configFields(Form::make('contact')); + + $mailerLiteField = collect($fields['mailerlite_field_mapping']['fields']) + ->firstWhere('handle', 'mailerlite_field')['field']; + + expect($mailerLiteField['type'])->toBe('mailer_lite_fields'); + expect($mailerLiteField['max_items'])->toBe(1); +}); + +it('offers the form fields as form_field options', function () { + $blueprint = Blueprint::makeFromFields([ + 'email' => ['type' => 'text'], + 'name' => ['type' => 'text'], + ])->setHandle('contact')->setNamespace('forms'); + + $form = Mockery::mock(FormContract::class); + $form->shouldReceive('handle')->andReturn('contact'); + $form->shouldReceive('blueprint')->andReturn($blueprint); + + $options = collect(StatamicMailerLiteServiceProvider::configFields($form)['mailerlite_field_mapping']['fields']) + ->firstWhere('handle', 'form_field')['field']['options']; + + expect($options)->toHaveKeys(['email', 'name']); +}); diff --git a/tests/Feature/MailerLiteFieldsFieldtypeTest.php b/tests/Feature/MailerLiteFieldsFieldtypeTest.php new file mode 100644 index 0000000..74d8fbe --- /dev/null +++ b/tests/Feature/MailerLiteFieldsFieldtypeTest.php @@ -0,0 +1,16 @@ +shouldReceive('setting')->with('api_key')->andReturn(null); + + Addon::shouldReceive('get')->with('concept7/statamic-mailerlite')->andReturn($addon); + + $items = (new MailerLiteFields)->getIndexItems(request()); + + expect($items)->toHaveCount(1); + expect($items->first())->toMatchArray(['id' => 'email']); +}); diff --git a/tests/Feature/SectionRegistrationTest.php b/tests/Feature/SectionRegistrationTest.php new file mode 100644 index 0000000..8ad9c39 --- /dev/null +++ b/tests/Feature/SectionRegistrationTest.php @@ -0,0 +1,18 @@ +shouldReceive('all')->andReturn(collect([$form])); + + app()->getProvider(StatamicMailerLiteServiceProvider::class)->bootAddon(); + + $sections = Form::extraConfigFor('contact'); + + expect($sections)->toHaveKey('mailer_lite'); + expect(array_keys($sections['mailer_lite']['fields'])) + ->toBe(['mailerlite_enabled', 'mailerlite_group', 'mailerlite_field_mapping']); +}); diff --git a/tests/Feature/SubmissionCreatedListenerTest.php b/tests/Feature/SubmissionCreatedListenerTest.php new file mode 100644 index 0000000..69728b8 --- /dev/null +++ b/tests/Feature/SubmissionCreatedListenerTest.php @@ -0,0 +1,147 @@ +shouldReceive('setting')->with('api_key')->andReturn($apiKey); + + Addon::shouldReceive('get')->with('concept7/statamic-mailerlite')->andReturn($addon); +} + +function submissionEvent(array $formData, array $submissionData = []): SubmissionCreated +{ + $form = Form::make('contact'); + + foreach ($formData as $key => $value) { + $form->set($key, $value); + } + + $submission = $form->makeSubmission(); + $submission->data($submissionData); + + return new SubmissionCreated($submission); +} + +function jobProperty(CreateSubscriberJob $job, string $name): mixed +{ + return (new ReflectionClass($job))->getProperty($name)->getValue($job); +} + +it('dispatches a subscriber job from the inline form config', function () { + fakeApiKey('key-123'); + + $event = submissionEvent([ + 'mailerlite_enabled' => true, + 'mailerlite_group' => '99', + 'mailerlite_field_mapping' => [ + ['form_field' => 'email', 'mailerlite_field' => 'email'], + ['form_field' => 'name', 'mailerlite_field' => 'name'], + ], + ], [ + 'email' => 'jane@example.com', + 'name' => 'Jane', + ]); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertPushed(CreateSubscriberJob::class, function (CreateSubscriberJob $job) { + return jobProperty($job, 'apiKey') === 'key-123' + && jobProperty($job, 'subscriberGroups') === ['99'] + && jobProperty($job, 'submissionData') === ['email' => 'jane@example.com', 'name' => 'Jane'] + && jobProperty($job, 'fieldMapping') === [ + ['form_field' => 'email', 'mailerlite_field' => 'email'], + ['form_field' => 'name', 'mailerlite_field' => 'name'], + ]; + }); +}); + +it('wraps a single selected group into an array', function () { + fakeApiKey('key-123'); + + $event = submissionEvent([ + 'mailerlite_enabled' => true, + 'mailerlite_group' => '42', + 'mailerlite_field_mapping' => [['form_field' => 'email', 'mailerlite_field' => 'email']], + ], ['email' => 'a@b.com']); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertPushed( + CreateSubscriberJob::class, + fn (CreateSubscriberJob $job) => jobProperty($job, 'subscriberGroups') === ['42'], + ); +}); + +it('dispatches without a group when none is selected', function () { + fakeApiKey('key-123'); + + $event = submissionEvent([ + 'mailerlite_enabled' => true, + 'mailerlite_field_mapping' => [['form_field' => 'email', 'mailerlite_field' => 'email']], + ], ['email' => 'a@b.com']); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertPushed( + CreateSubscriberJob::class, + fn (CreateSubscriberJob $job) => jobProperty($job, 'subscriberGroups') === [], + ); +}); + +it('does not dispatch when sync is disabled', function () { + fakeApiKey('key-123'); + + $event = submissionEvent([ + 'mailerlite_enabled' => false, + 'mailerlite_field_mapping' => [['form_field' => 'email', 'mailerlite_field' => 'email']], + ], ['email' => 'a@b.com']); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertNothingPushed(); +}); + +it('does not dispatch when the form has no MailerLite config', function () { + fakeApiKey('key-123'); + + (new SubmissionCreatedListener)->handle(submissionEvent([], ['email' => 'a@b.com'])); + + Queue::assertNothingPushed(); +}); + +it('does not dispatch when the api key is missing', function () { + fakeApiKey(null); + + $event = submissionEvent([ + 'mailerlite_enabled' => true, + 'mailerlite_field_mapping' => [['form_field' => 'email', 'mailerlite_field' => 'email']], + ], ['email' => 'a@b.com']); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertNothingPushed(); +}); + +it('does not dispatch when the field mapping is empty', function () { + fakeApiKey('key-123'); + + $event = submissionEvent([ + 'mailerlite_enabled' => true, + 'mailerlite_field_mapping' => [], + ], ['email' => 'a@b.com']); + + (new SubmissionCreatedListener)->handle($event); + + Queue::assertNothingPushed(); +});