diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php index f77a364..b549e67 100644 --- a/app/Http/Controllers/SitemapController.php +++ b/app/Http/Controllers/SitemapController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; use Spatie\Sitemap\Sitemap; use Spatie\Sitemap\Tags\Url; +use App\Models\Template; class SitemapController extends Controller { @@ -20,6 +21,7 @@ class SitemapController extends Controller ['/login', 0.4], ['/register', 0.4], ['/password/reset', 0.3], + ['/templates', 0.9], ]; public function getSitemap(Request $request) @@ -28,6 +30,7 @@ class SitemapController extends Controller foreach ($this->urls as $url) { $sitemap->add($this->createUrl($url[0], $url[1])); } + $this->addTemplatesUrls($sitemap); return $sitemap->toResponse($request); } @@ -36,4 +39,13 @@ class SitemapController extends Controller { return Url::create($url)->setPriority($priority)->setChangeFrequency($frequency); } + + private function addTemplatesUrls(Sitemap $sitemap) + { + Template::chunk(100, function ($templates) use ($sitemap) { + foreach ($templates as $template) { + $sitemap->add($this->createUrl('/templates/' . $template->slug, 0.7)); + } + }); + } } diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php new file mode 100644 index 0000000..35190cd --- /dev/null +++ b/app/Http/Controllers/TemplateController.php @@ -0,0 +1,32 @@ +middleware('admin'); + + // Create template + $template = $request->getTemplate(); + $template->save(); + + return $this->success([ + 'message' => 'Template created.', + 'template_id' => $template->id + ]); + } + +} diff --git a/app/Http/Requests/Templates/CreateTemplateRequest.php b/app/Http/Requests/Templates/CreateTemplateRequest.php new file mode 100644 index 0000000..43e2acc --- /dev/null +++ b/app/Http/Requests/Templates/CreateTemplateRequest.php @@ -0,0 +1,45 @@ + + */ + public function rules() + { + return [ + 'form' => 'required|array', + 'name' => 'required|string|max:60', + 'slug' => 'required|string|unique:templates', + 'description' => 'required|string|max:2000', + 'image_url' => 'required|string', + 'questions' => 'array', + ]; + } + + public function getTemplate() : Template + { + $structure = $this->form; + $ignoreKeys = ['id','creator','creator_id','created_at','updated_at','extra','workspace','submissions','submissions_count','views','views_count']; + foreach($structure as $key=>$val){ + if(in_array($key, $ignoreKeys)){ + $structure[$key] = null; + } + } + return new Template([ + 'name' => $this->name, + 'slug' => $this->slug, + 'description' => $this->description, + 'image_url' => $this->image_url, + 'structure' => $structure, + 'questions' => $this->questions ?? [] + ]); + } +} diff --git a/app/Models/Template.php b/app/Models/Template.php new file mode 100644 index 0000000..cbf541b --- /dev/null +++ b/app/Models/Template.php @@ -0,0 +1,32 @@ + 'array', + 'questions' => 'array', + ]; + + public function setDescriptionAttribute($value) + { + // Strip out unwanted html + $this->attributes['description'] = Purify::clean($value); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index cc2b603..436cb87 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -18,8 +18,6 @@ class User extends Authenticatable implements JWTSubject //, MustVerifyEmail { use Notifiable, HasFactory, Billable; - const ADMINS = ['julien@notionforms.io']; - /** * The attributes that are mass assignable. * @@ -98,7 +96,7 @@ class User extends Authenticatable implements JWTSubject //, MustVerifyEmail public function getAdminAttribute() { - return in_array($this->email, self::ADMINS); + return in_array($this->email, config('services.admin_emails')); } /** diff --git a/config/services.php b/config/services.php index eaa8e0d..52c3257 100644 --- a/config/services.php +++ b/config/services.php @@ -50,5 +50,7 @@ return [ 'google_analytics_code' => env('GOOGLE_ANALYTICS_CODE'), 'amplitude_code' => env('AMPLITUDE_CODE'), - 'crisp_website_id' => env('CRISP_WEBSITE_ID') + 'crisp_website_id' => env('CRISP_WEBSITE_ID'), + + 'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? '') ]; diff --git a/database/migrations/2022_09_22_092205_create_templates_table.php b/database/migrations/2022_09_22_092205_create_templates_table.php new file mode 100644 index 0000000..4642c58 --- /dev/null +++ b/database/migrations/2022_09_22_092205_create_templates_table.php @@ -0,0 +1,36 @@ +id(); + $table->timestamps(); + $table->string('name'); + $table->string('slug'); + $table->text('description'); + $table->string('image_url'); + $table->jsonb('structure')->default('{}'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('templates'); + } +}; diff --git a/database/migrations/2022_09_26_084721_add_questions_to_templates.php b/database/migrations/2022_09_26_084721_add_questions_to_templates.php new file mode 100644 index 0000000..449fe40 --- /dev/null +++ b/database/migrations/2022_09_26_084721_add_questions_to_templates.php @@ -0,0 +1,32 @@ +jsonb('questions')->default('{}'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('templates', function (Blueprint $table) { + $table->dropColumn('questions'); + }); + } +}; diff --git a/resources/js/components/open/forms/components/FormFieldsEditor.vue b/resources/js/components/open/forms/components/FormFieldsEditor.vue index d4ddc35..d3db48c 100644 --- a/resources/js/components/open/forms/components/FormFieldsEditor.vue +++ b/resources/js/components/open/forms/components/FormFieldsEditor.vue @@ -224,7 +224,7 @@ export default { }, init() { if (this.$route.name === 'forms.create') { // Set Default fields - this.formFields = this.getDefaultFields() + this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties): this.getDefaultFields() } else { this.formFields = clonedeep(this.form.properties).map((field) => { // Add more field properties diff --git a/resources/js/components/open/forms/components/FormSubmissions.vue b/resources/js/components/open/forms/components/FormSubmissions.vue index 7837100..5bb97ef 100644 --- a/resources/js/components/open/forms/components/FormSubmissions.vue +++ b/resources/js/components/open/forms/components/FormSubmissions.vue @@ -53,7 +53,6 @@ export default { }, watch: { form () { - debugger if(!this.form){ return } diff --git a/resources/js/components/pages/OpenFormFooter.vue b/resources/js/components/pages/OpenFormFooter.vue index 8c836ae..213413b 100644 --- a/resources/js/components/pages/OpenFormFooter.vue +++ b/resources/js/components/pages/OpenFormFooter.vue @@ -21,6 +21,13 @@ +
  • + + Templates + +
  • + +
    +
    +
    +

    + Create template +

    +

    + New template will be create from your form {{form.title}}. + Template will be public for all to create form quickly. +

    +
    +
    + + + + + +
    +
    + Create + Close +
    +
    +
    +
    + + + + \ No newline at end of file diff --git a/resources/js/components/templates/QuestionsEditor.vue b/resources/js/components/templates/QuestionsEditor.vue new file mode 100644 index 0000000..c81702f --- /dev/null +++ b/resources/js/components/templates/QuestionsEditor.vue @@ -0,0 +1,80 @@ + + + + \ No newline at end of file diff --git a/resources/js/pages/forms/create.vue b/resources/js/pages/forms/create.vue index 43b66dc..7ce727c 100644 --- a/resources/js/pages/forms/create.vue +++ b/resources/js/pages/forms/create.vue @@ -51,12 +51,21 @@ diff --git a/resources/js/pages/templates/templates.vue b/resources/js/pages/templates/templates.vue new file mode 100644 index 0000000..5f01b0a --- /dev/null +++ b/resources/js/pages/templates/templates.vue @@ -0,0 +1,91 @@ + + + diff --git a/resources/js/router/routes.js b/resources/js/router/routes.js index 844e7e1..a0a8cb8 100644 --- a/resources/js/router/routes.js +++ b/resources/js/router/routes.js @@ -52,5 +52,9 @@ export default [ { path: '/integrations', name: 'integrations', component: page('integrations.vue') }, { path: '/forms/:slug', name: 'forms.show_public', component: page('forms/show-public.vue') }, + // Templates + { path: '/templates', name: 'templates', component: page('templates/templates.vue') }, + { path: '/templates/:slug', name: 'templates.show', component: page('templates/show.vue') }, + { path: '*', component: page('errors/404.vue') } ] diff --git a/resources/js/store/modules/open/templates.js b/resources/js/store/modules/open/templates.js new file mode 100644 index 0000000..a03936f --- /dev/null +++ b/resources/js/store/modules/open/templates.js @@ -0,0 +1,55 @@ +import Vue from 'vue' +import axios from 'axios' + +// state +export const state = { + content: [], + loading: false +} + +// getters +export const getters = { + getById: (state) => (id) => { + if (state.content.length === 0) return null + return state.content.find(item => item.id === id) + }, + getBySlug: (state) => (slug) => { + if (state.content.length === 0) return null + return state.content.find(item => item.slug === slug) + }, +} + +// mutations +export const mutations = { + set (state, items) { + state.content = items + }, + addOrUpdate (state, item) { + state.content = state.content.filter((val) => val.id !== item.id) + state.content.push(item) + }, + startLoading () { + state.loading = true + }, + stopLoading () { + state.loading = false + } +} + +// actions +export const actions = { + load (context) { + context.commit('set', []) + context.commit('startLoading') + return axios.get('/api/templates').then((response) => { + context.commit('set', response.data) + context.commit('stopLoading') + }) + }, + loadIfEmpty ({ context, dispatch, state }) { + if (state.content.length === 0) { + return dispatch('load') + } + return Promise.resolve() + }, +} diff --git a/routes/api.php b/routes/api.php index 4a15c46..52f8ce8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -15,6 +15,7 @@ use App\Http\Controllers\Forms\PublicFormController; use App\Http\Controllers\Forms\FormSubmissionController; use App\Http\Controllers\Forms\FormController; use App\Http\Controllers\WorkspaceController; +use App\Http\Controllers\TemplateController; use App\Http\Controllers\Forms\Integration\FormZapierWebhookController; use Illuminate\Support\Facades\Route; @@ -150,3 +151,7 @@ Route::prefix('forms')->name('forms.')->group(function () { Route::prefix('content')->name('content.')->group(function () { Route::get('changelog/entries', [\App\Http\Controllers\Content\ChangelogController::class, 'index'])->name('changelog.entries'); }); + +// Templates +Route::get('templates', [TemplateController::class, 'index'])->name('templates.show'); +Route::post('templates', [TemplateController::class, 'create'])->name('templates.create'); \ No newline at end of file diff --git a/tests/Feature/TemplateTest.php b/tests/Feature/TemplateTest.php new file mode 100644 index 0000000..172ca20 --- /dev/null +++ b/tests/Feature/TemplateTest.php @@ -0,0 +1,25 @@ +actingAsUser(); + + // Create Form + $workspace = $this->createUserWorkspace($user); + $form = $this->makeForm($user, $workspace); + + // Create Template + $templateData = [ + 'name' => 'Demo Template', + 'slug' => 'demo_template', + 'description' => 'Some description here...', + 'image_url' => 'https://d3ietpyl4f2d18.cloudfront.net/6c35a864-ee3a-4039-80a4-040b6c20ac60/img/pages/welcome/product_cover.jpg', + 'form' => $form->getAttributes(), + 'questions' => [['question'=>'Question 1','answer'=>'Answer 1 will be here...']] + ]; + $this->postJson(route('templates.create', $templateData)) + ->assertSuccessful() + ->assertJson([ + 'type' => 'success', + 'message' => 'Template created.' + ]); +}); \ No newline at end of file