├── LICENSE ├── composer.json ├── database ├── factories │ ├── AnswerFactory.php │ ├── FieldFactory.php │ ├── FormFactory.php │ └── SectionFactory.php └── migrations │ ├── 2019_02_28_105600_create_formoj_forms_table.php │ ├── 2019_02_28_105900_create_formoj_sections_table.php │ ├── 2019_02_28_110000_create_formoj_fields_table.php │ ├── 2019_03_04_113200_create_formoj_answers_table.php │ ├── 2019_09_24_160300_add_boolean_to_hide_form_title.php │ ├── 2019_09_24_163900_add_boolean_to_hide_section_title.php │ └── 2019_09_25_091200_add_field_identifier.php ├── resources └── lang │ ├── en │ ├── form.php │ └── sharp.php │ ├── fr │ ├── form.php │ └── sharp.php │ └── pt-BR │ ├── form.php │ └── sharp.php └── src ├── Console └── SendFormojNotificationsForYesterday.php ├── Controllers ├── FormojAnswerController.php ├── FormojFormController.php ├── FormojFormFillController.php ├── FormojSectionController.php ├── FormojUploadController.php └── Requests │ ├── FormRequest.php │ ├── SectionRequest.php │ └── UploadRequest.php ├── FormojServiceProvider.php ├── Job ├── ExportAnswersToXls.php ├── SendDailyNotifications.php └── Utils │ └── AnswersExcelCollection.php ├── Models ├── Answer.php ├── Creators │ ├── FieldCreator.php │ ├── SelectFieldCreator.php │ ├── TextFieldCreator.php │ ├── TextareaFieldCreator.php │ └── UploadFieldCreator.php ├── Field.php ├── Form.php ├── Resources │ ├── AnswerResource.php │ ├── FieldResource.php │ ├── FormResource.php │ └── SectionResource.php └── Section.php ├── Notifications ├── FormojFormWasAnsweredToday.php └── FormojFormWasJustAnswered.php ├── Sharp ├── Commands │ ├── FormojAnswerDownloadFilesCommand.php │ └── FormojAnswerExportCommand.php ├── Entities │ ├── FormojAnswerEntity.php │ ├── FormojFieldEntity.php │ ├── FormojFormEntity.php │ ├── FormojReplyEntity.php │ └── FormojSectionEntity.php ├── FormojAnswerSharpEntityList.php ├── FormojAnswerSharpShow.php ├── FormojFieldSharpEntityList.php ├── FormojFieldSharpForm.php ├── FormojFormSharpEntityList.php ├── FormojFormSharpForm.php ├── FormojFormSharpShow.php ├── FormojReplySharpEntityList.php ├── FormojSectionSharpEntityList.php ├── FormojSectionSharpForm.php └── FormojSectionSharpShow.php ├── api.php └── config.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Code16 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code16/formoj", 3 | "description": "Customizable form renderer", 4 | "authors": [{ 5 | "name": "Philippe Lonchampt", 6 | "email": "philippe@code16.fr" 7 | }, { 8 | "name": "Antoine Guingand", 9 | "email": "antoine@code16.fr" 10 | }], 11 | "require": { 12 | "php": "^8.2|^8.3|^8.4", 13 | "laravel/framework": "^10.0|^11.0|^12.0", 14 | "nesbot/carbon": "^2.0|^3.0", 15 | "maatwebsite/excel": "^3.1", 16 | "ext-zip": "*" 17 | }, 18 | "require-dev": { 19 | "fakerphp/faker": "^1.19.0", 20 | "mockery/mockery": "^1.3.0", 21 | "phpunit/phpunit": "^10.0|^11.0", 22 | "doctrine/dbal": "^3.0", 23 | "orchestra/testbench": "6.*|7.*|8.*|9.*|10.*", 24 | "code16/sharp": "^9.0", 25 | "ext-json": "*" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Code16\\Formoj\\": "src/", 30 | "Code16\\Formoj\\Database\\Factories\\": "database/factories/" 31 | } 32 | }, 33 | "minimum-stability": "dev", 34 | "prefer-stable": true, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Code16\\Formoj\\Tests\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "Code16\\Formoj\\FormojServiceProvider" 44 | ] 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/factories/AnswerFactory.php: -------------------------------------------------------------------------------- 1 | [ 27 | "field 1" => $this->faker->sentence 28 | ], 29 | 'form_id' => function() { 30 | return Form::factory()->create()->id; 31 | } 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/factories/FieldFactory.php: -------------------------------------------------------------------------------- 1 | faker->randomElement([ 27 | \Code16\Formoj\Models\Field::TYPE_TEXT, 28 | \Code16\Formoj\Models\Field::TYPE_TEXTAREA, 29 | \Code16\Formoj\Models\Field::TYPE_SELECT, 30 | \Code16\Formoj\Models\Field::TYPE_UPLOAD, 31 | \Code16\Formoj\Models\Field::TYPE_RATING, 32 | ]); 33 | 34 | $fieldAttributes = []; 35 | 36 | if($type == \Code16\Formoj\Models\Field::TYPE_SELECT) { 37 | for($i=0; $ifaker->word; 39 | } 40 | if($this->faker->boolean(40)) { 41 | $fieldAttributes["multiple"] = true; 42 | $fieldAttributes["max_options"] = $this->faker->boolean() ? $this->faker->numberBetween(2, 4) : null; 43 | } 44 | } elseif($type == \Code16\Formoj\Models\Field::TYPE_UPLOAD) { 45 | $fieldAttributes["max_size"] = 4; 46 | $fieldAttributes["accept"] = ".jpeg,.jpg,.gif,.png,.pdf"; 47 | } elseif ($type == \Code16\Formoj\Models\Field::TYPE_RATING) { 48 | $fieldAttributes["lowest_label"] = $this->faker->word; 49 | $fieldAttributes["highest_label"] = $this->faker->word; 50 | } 51 | 52 | $label = $this->faker->words(3, true); 53 | 54 | return [ 55 | 'label' => $label, 56 | 'identifier' => Str::slug($label,'_'), 57 | 'help_text' => $this->faker->boolean(25) ? $this->faker->paragraph : null, 58 | 'type' => $type, 59 | 'field_attributes' => $fieldAttributes, 60 | 'required' => $type == \Code16\Formoj\Models\Field::TYPE_HEADING ? false : $this->faker->boolean(40), 61 | 'section_id' => function() { 62 | return Section::factory()->create()->id; 63 | } 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /database/factories/FormFactory.php: -------------------------------------------------------------------------------- 1 | faker->boolean(); 25 | $publishedDate = $hasPublishedDate ? $this->faker->dateTimeBetween('-10 days', '+5 days') : null; 26 | $unpublishedDate = $this->faker->boolean() 27 | ? ($hasPublishedDate ? $this->faker->dateTimeBetween($publishedDate, '+15 days') : $this->faker->dateTimeBetween('-10 days', '+5 days')) 28 | : null; 29 | 30 | return [ 31 | 'title' => $this->faker->words(2, true), 32 | 'is_title_hidden' => $this->faker->boolean(), 33 | 'description' => $this->faker->paragraph, 34 | 'published_at' => $publishedDate, 35 | 'unpublished_at' => $unpublishedDate, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/factories/SectionFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->words(3, true), 27 | 'is_title_hidden' => $this->faker->boolean(), 28 | 'description' => $this->faker->paragraph, 29 | 'form_id' => function() { 30 | return Form::factory()->create()->id; 31 | } 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2019_02_28_105600_create_formoj_forms_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string("title")->nullable(); 19 | $table->text("description")->nullable(); 20 | $table->dateTime('published_at')->nullable(); 21 | $table->dateTime('unpublished_at')->nullable(); 22 | $table->text("success_message")->nullable(); 23 | $table->string("administrator_email")->nullable(); 24 | $table->string("notifications_strategy")->default("none"); 25 | 26 | $table->timestamps(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2019_02_28_105900_create_formoj_sections_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string("title"); 19 | $table->text("description")->nullable(); 20 | $table->unsignedSmallInteger("order")->default(100); 21 | 22 | $table->unsignedInteger('form_id'); 23 | $table->foreign('form_id') 24 | ->references('id') 25 | ->on('formoj_forms') 26 | ->onDelete('cascade'); 27 | 28 | $table->timestamps(); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/migrations/2019_02_28_110000_create_formoj_fields_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string("label"); 19 | $table->boolean('required')->default(false); 20 | $table->text("help_text")->nullable(); 21 | $table->unsignedSmallInteger("order")->default(100); 22 | $table->string("type"); // text, textarea, select 23 | 24 | $table->longText("field_attributes"); 25 | 26 | // // Text, textarea 27 | // $table->unsignedSmallInteger("max_length")->nullable(); 28 | // 29 | // // Textarea 30 | // $table->unsignedSmallInteger("rows_count")->nullable(); 31 | // 32 | // // Select 33 | // $table->longText("values")->nullable(); 34 | // $table->unsignedSmallInteger("max_values")->nullable(); 35 | // $table->boolean("multiple")->default(false); 36 | 37 | $table->unsignedInteger('section_id'); 38 | $table->foreign('section_id') 39 | ->references('id') 40 | ->on('formoj_sections') 41 | ->onDelete('cascade'); 42 | 43 | $table->timestamps(); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /database/migrations/2019_03_04_113200_create_formoj_answers_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->longText("content"); 19 | 20 | $table->unsignedInteger('form_id'); 21 | $table->foreign('form_id') 22 | ->references('id') 23 | ->on('formoj_forms') 24 | ->onDelete('cascade'); 25 | 26 | $table->timestamps(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /database/migrations/2019_09_24_160300_add_boolean_to_hide_form_title.php: -------------------------------------------------------------------------------- 1 | boolean('is_title_hidden')->default(false); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/migrations/2019_09_24_163900_add_boolean_to_hide_section_title.php: -------------------------------------------------------------------------------- 1 | boolean('is_title_hidden')->default(false); 18 | }); 19 | } 20 | } -------------------------------------------------------------------------------- /database/migrations/2019_09_25_091200_add_field_identifier.php: -------------------------------------------------------------------------------- 1 | string("identifier"); 22 | } else { 23 | $table->string("identifier")->default(''); 24 | } 25 | }); 26 | 27 | $this->fillEmptyFieldIdentifiers(); 28 | $this->replaceAnswerFieldsWithIdentifiers(); 29 | } 30 | 31 | public function fillEmptyFieldIdentifiers() 32 | { 33 | Field::where('identifier','=','') 34 | ->get() 35 | ->each(function(Field $field) { 36 | $slug = $field->label ? Str::slug($field->label,'_') : 'unnamed_field'; 37 | $generatedIdentifier = $slug; 38 | $i = 1; 39 | 40 | while( 41 | Field::join('formoj_sections','formoj_sections.id','=','section_id') 42 | ->where('form_id','=',$field->section->form->id) 43 | ->where('formoj_fields.identifier','=',$generatedIdentifier) 44 | ->exists() 45 | ) { 46 | $generatedIdentifier = $slug . '_' . $i; 47 | $i++; 48 | } 49 | 50 | $field->identifier = $generatedIdentifier; 51 | $field->save(); 52 | }); 53 | } 54 | 55 | public function replaceAnswerFieldsWithIdentifiers() 56 | { 57 | Answer::get() 58 | ->each(function(Answer $answer) { 59 | $newContent = []; 60 | 61 | foreach($answer->content as $key => $value) { 62 | $field = Field::join('formoj_sections','formoj_fields.section_id','=','formoj_sections.id') 63 | ->where('form_id','=',$answer->form_id) 64 | ->where('label','=',$key) 65 | ->first(); 66 | 67 | if($field) { 68 | $newContent[$field->identifier] = $value; 69 | } else { 70 | $newContent[$key] = $value; 71 | } 72 | 73 | } 74 | $answer->content = $newContent; 75 | $answer->save(); 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /resources/lang/en/form.php: -------------------------------------------------------------------------------- 1 | 'Thank you, your answer has been registered', 6 | 'form_too_soon' => "This form isn't available yet.", 7 | 'form_too_late' => "This form is no longer available.", 8 | 9 | 'notifications' => [ 10 | 'new_answer' => [ 11 | 'subject' => 'New answer to the :form form', 12 | 'greeting' => 'Answer of :date', 13 | ], 14 | 'daily_answers' => [ 15 | 'subject' => 'Answers of the day of the form :form', 16 | 'greeting' => ':count answer(s)', 17 | ] 18 | ] 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/sharp.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'form' => "Form", 10 | 'section' => "Section", 11 | 'field' => "Field", 12 | 'answer' => "Answer", 13 | ], 14 | 15 | 'forms' => [ 16 | 'no_title' => "(without title)", 17 | 'notification_strategies' => [ 18 | Form::NOTIFICATION_STRATEGY_EVERY => "On every answer", 19 | Form::NOTIFICATION_STRATEGY_GROUPED => "Once a day", 20 | Form::NOTIFICATION_STRATEGY_NONE => "Never", 21 | ], 22 | 'fields' => [ 23 | "title" => [ 24 | "label" => "Title" 25 | ], 26 | "is_title_hidden" => [ 27 | "label" => "Hide form title" 28 | ], 29 | "description" => [ 30 | "label" => "Description" 31 | ], 32 | "published_at" => [ 33 | "label" => "From" 34 | ], 35 | "unpublished_at" => [ 36 | "label" => "to" 37 | ], 38 | "success_message" => [ 39 | "label" => "Message displayed after posting the form", 40 | "help_text" => "This text will be shown to the user when posting his answer. If let blank, a standard message will be used.", 41 | ], 42 | "notifications_strategy" => [ 43 | "label" => "Sending frequency" 44 | ], 45 | "administrator_email" => [ 46 | "label" => "Recipient e-mail address" 47 | ], 48 | "fieldsets" => [ 49 | "title" => "Form title", 50 | "dates" => "Publication dates (optional)", 51 | "notifications" => "Notifications", 52 | ], 53 | ], 54 | "list" => [ 55 | "columns" => [ 56 | "ref_label" => "Ref", 57 | "title_label" => "Title", 58 | "description_label" => "Description", 59 | "published_at_label" => "publication dates", 60 | "sections_label" => "Sections", 61 | "answers_label" => "Answers", 62 | ], 63 | "data" => [ 64 | "dates" => [ 65 | "both" => "From %s
to %s", 66 | "from" => "From %s", 67 | "to" => "Until %s", 68 | ] 69 | ] 70 | ] 71 | ], 72 | 73 | 'sections' => [ 74 | "list" => [ 75 | "columns" => [ 76 | "title_label" => "Title", 77 | "description_label" => "Description", 78 | ], 79 | "data" => [ 80 | "title" => [ 81 | "is_hidden" => "Hidden", 82 | ] 83 | ] 84 | ], 85 | "fields" => [ 86 | "title" => [ 87 | "label" => "Title" 88 | ], 89 | "is_title_hidden" => [ 90 | "label" => "Hide title" 91 | ], 92 | "description" => [ 93 | "label" => "Description" 94 | ], 95 | "fields" => [ 96 | "label" => "Fields" 97 | ] 98 | ], 99 | ], 100 | 101 | 'fields' => [ 102 | 'types' => [ 103 | Field::TYPE_TEXT => "Simple text", 104 | Field::TYPE_TEXTAREA => "Multi-rows text", 105 | Field::TYPE_SELECT => "Dropdown list", 106 | Field::TYPE_HEADING => "Inter-title", 107 | Field::TYPE_UPLOAD => "File", 108 | Field::TYPE_RATING => "Rating", 109 | ], 110 | 'fields' => [ 111 | "label" => [ 112 | "label" => "Label" 113 | ], 114 | "identifier" => [ 115 | "label" => "Unique identifier", 116 | "help_text" => "Technical field, must be unique for the whole form (not displayed to user). Please use _ separator (exemple: other_reason_1)" 117 | ], 118 | "type" => [ 119 | "label" => "" 120 | ], 121 | "help_text" => [ 122 | "label" => "Help text" 123 | ], 124 | "required" => [ 125 | "text" => "Required field" 126 | ], 127 | "max_length" => [ 128 | "label" => "Maximum length", 129 | "help_text" => "In number of chars", 130 | ], 131 | "rows_count" => [ 132 | "label" => "Lines number", 133 | ], 134 | "multiple" => [ 135 | "text" => "Allow multiple choices" 136 | ], 137 | "radios" => [ 138 | "text" => "Display as radio buttons (can't be multiple)" 139 | ], 140 | "max_options" => [ 141 | "label" => "Maximum number of choices" 142 | ], 143 | "options" => [ 144 | "label" => "Possible values", 145 | "add_label" => "Add a value", 146 | ], 147 | "max_size" => [ 148 | "label" => "Max size", 149 | "help_text" => "Integer, expressed in MB.", 150 | ], 151 | "accept" => [ 152 | "label" => "Allowed file extensions (optional)", 153 | "help_text" => "Extensions list separated by commas, without space.", 154 | ], 155 | "fieldsets" => [ 156 | "identifiers" => "Identifiers", 157 | ], 158 | "lowest_label" => [ 159 | "label" => "Lowest rating label" 160 | ], 161 | "highest_label" => [ 162 | "label" => "Highest rating label" 163 | ], 164 | ], 165 | "list" => [ 166 | "columns" => [ 167 | "type_label" => "", 168 | "label_label" => "Label", 169 | "help_text_label" => "Help text", 170 | ], 171 | "data" => [ 172 | "label" => [ 173 | "required" => "required" 174 | ] 175 | ] 176 | ] 177 | ], 178 | 179 | 'answers' => [ 180 | "list" => [ 181 | "columns" => [ 182 | "created_at_label" => "Date", 183 | "content_label" => "", 184 | ], 185 | "data" => [ 186 | "label" => [ 187 | "required" => "required" 188 | ] 189 | ] 190 | ], 191 | 'fields' => [ 192 | 'replies' => [ 193 | 'label' => 'Content' 194 | ] 195 | ], 196 | 'commands' => [ 197 | 'export' => "Export answers (XLS)", 198 | 'download_files' => "Download answer attachments", 199 | ], 200 | 'errors' => [ 201 | 'no_file_to_download' => "This answer does not contains any File attachment.", 202 | ] 203 | ], 204 | 205 | 'replies' => [ 206 | "list" => [ 207 | "columns" => [ 208 | "label_label" => "Field", 209 | "value_label" => "Value", 210 | ], 211 | ], 212 | ] 213 | 214 | ]; 215 | -------------------------------------------------------------------------------- /resources/lang/fr/form.php: -------------------------------------------------------------------------------- 1 | 'Merci, votre saisie a bien été prise en compte.', 6 | 'form_too_soon' => "Ce formulaire n'est pas encore disponible publiquement.", 7 | 'form_too_late' => "Ce formulaire n'est plus disponible publiquement.", 8 | 9 | 'notifications' => [ 10 | 'new_answer' => [ 11 | 'subject' => 'Nouvelle réponse au formulaire :form', 12 | 'greeting' => 'Réponse du :date', 13 | ], 14 | 'daily_answers' => [ 15 | 'subject' => 'Réponses du jour au formulaire :form', 16 | 'greeting' => ':count réponse(s)', 17 | ] 18 | ] 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/fr/sharp.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'form' => "Formulaire", 10 | 'section' => "Section", 11 | 'field' => "Champ", 12 | 'answer' => "Réponse", 13 | ], 14 | 15 | 'forms' => [ 16 | 'no_title' => "(sans titre)", 17 | 'notification_strategies' => [ 18 | Form::NOTIFICATION_STRATEGY_EVERY => "A chaque réponse", 19 | Form::NOTIFICATION_STRATEGY_GROUPED => "Une fois par jour", 20 | Form::NOTIFICATION_STRATEGY_NONE => "Jamais", 21 | ], 22 | 'fields' => [ 23 | "title" => [ 24 | "label" => "Titre" 25 | ], 26 | "is_title_hidden" => [ 27 | "label" => "Masquer le titre du formulaire" 28 | ], 29 | "description" => [ 30 | "label" => "Description" 31 | ], 32 | "published_at" => [ 33 | "label" => "Du" 34 | ], 35 | "unpublished_at" => [ 36 | "label" => "au" 37 | ], 38 | "success_message" => [ 39 | "label" => "Message affiché en fin de saisie du formulaire", 40 | "help_text" => "Ce texte sera affiché à l'utilisateur au moment de la validation de sa réponse. S'il est laissé vide, un message standard le remplacera.", 41 | ], 42 | "notifications_strategy" => [ 43 | "label" => "Périodicité d'envoi" 44 | ], 45 | "administrator_email" => [ 46 | "label" => "Adresse email de réception" 47 | ], 48 | "fieldsets" => [ 49 | "title" => "Titre du formulaire", 50 | "dates" => "Dates de publication (facultatives)", 51 | "notifications" => "Notifications", 52 | ], 53 | ], 54 | "list" => [ 55 | "columns" => [ 56 | "ref_label" => "Ref", 57 | "title_label" => "Titre", 58 | "description_label" => "Description", 59 | "published_at_label" => "Dates publication", 60 | "sections_label" => "Sections", 61 | "answers_label" => "Réponses", 62 | ], 63 | "data" => [ 64 | "dates" => [ 65 | "both" => "Du %s
au %s", 66 | "from" => "À partir du %s", 67 | "to" => "Jusqu'au %s", 68 | ] 69 | ] 70 | ] 71 | ], 72 | 73 | 'sections' => [ 74 | "list" => [ 75 | "columns" => [ 76 | "title_label" => "Titre", 77 | "description_label" => "Description", 78 | ], 79 | "data" => [ 80 | "title" => [ 81 | "is_hidden" => "Masqué", 82 | ] 83 | ] 84 | ], 85 | "fields" => [ 86 | "title" => [ 87 | "label" => "Titre" 88 | ], 89 | "is_title_hidden" => [ 90 | "label" => "Masquer le titre" 91 | ], 92 | "description" => [ 93 | "label" => "Description" 94 | ], 95 | "fields" => [ 96 | "label" => "Champs" 97 | ], 98 | ], 99 | ], 100 | 101 | 'fields' => [ 102 | 'types' => [ 103 | Field::TYPE_TEXT => "Texte simple", 104 | Field::TYPE_TEXTAREA => "Texte multilignes", 105 | Field::TYPE_SELECT => "Liste déroulante", 106 | Field::TYPE_HEADING => "Intertitre", 107 | Field::TYPE_UPLOAD => "Fichier", 108 | Field::TYPE_RATING => "Notation", 109 | ], 110 | 'fields' => [ 111 | "label" => [ 112 | "label" => "Libellé" 113 | ], 114 | "identifier" => [ 115 | "label" => "Identifiant unique", 116 | "help_text" => "Champ technique qui doit être unique pour tout le formulaire (non affiché à l'utilisateur). Utilisez le séparateur _ (exemple: autres_raisons_1)" 117 | ], 118 | "type" => [ 119 | "label" => "" 120 | ], 121 | "help_text" => [ 122 | "label" => "Texte d'aide" 123 | ], 124 | "required" => [ 125 | "text" => "Saisie obligatoire" 126 | ], 127 | "max_length" => [ 128 | "label" => "Longueur maximale", 129 | "help_text" => "En nombre de caractères", 130 | ], 131 | "rows_count" => [ 132 | "label" => "Nombre de lignes", 133 | ], 134 | "multiple" => [ 135 | "text" => "Autoriser plusieurs réponses" 136 | ], 137 | "radios" => [ 138 | "text" => "Afficher sous forme de boutons radio (contraint une seule réponse)" 139 | ], 140 | "max_options" => [ 141 | "label" => "Nombre maximum de réponses" 142 | ], 143 | "options" => [ 144 | "label" => "Valeurs possibles", 145 | "add_label" => "Ajouter une valeur", 146 | ], 147 | "max_size" => [ 148 | "label" => "Taille maximale", 149 | "help_text" => "Chiffre entier, exprimé en Mo.", 150 | ], 151 | "accept" => [ 152 | "label" => "Extensions acceptées (facultatif)", 153 | "help_text" => "Liste d'extensions avec le point, séparés par des virgules, sans espace.", 154 | ], 155 | "fieldsets" => [ 156 | "identifiers" => "Identifiants", 157 | ], 158 | "lowest_label" => [ 159 | "label" => "Libellé de la note la plus basse" 160 | ], 161 | "highest_label" => [ 162 | "label" => "Libellé de la note la plus haute" 163 | ], 164 | ], 165 | "list" => [ 166 | "columns" => [ 167 | "type_label" => "", 168 | "label_label" => "Libellé", 169 | "help_text_label" => "Texte d'aide", 170 | ], 171 | "data" => [ 172 | "label" => [ 173 | "required" => "obligatoire" 174 | ] 175 | ] 176 | ] 177 | ], 178 | 179 | 'answers' => [ 180 | "list" => [ 181 | "columns" => [ 182 | "created_at_label" => "Date", 183 | "content_label" => "", 184 | ], 185 | "data" => [ 186 | "label" => [ 187 | "required" => "obligatoire" 188 | ] 189 | ] 190 | ], 191 | 'fields' => [ 192 | 'replies' => [ 193 | 'label' => 'Contenu' 194 | ] 195 | ], 196 | 'commands' => [ 197 | 'export' => "Exporter les réponses au format XLS", 198 | 'download_files' => "Télécharger les fichiers joints de cette réponse", 199 | ], 200 | 'errors' => [ 201 | 'no_file_to_download' => "Cette réponse en contient aucun fichier en pièce jointe.", 202 | ] 203 | ], 204 | 205 | 'replies' => [ 206 | "list" => [ 207 | "columns" => [ 208 | "label_label" => "Champ", 209 | "value_label" => "Valeur", 210 | ], 211 | ], 212 | ] 213 | 214 | ]; 215 | -------------------------------------------------------------------------------- /resources/lang/pt-BR/form.php: -------------------------------------------------------------------------------- 1 | 'Obrigado, sua resposta foi registrada', 6 | 'form_too_soon' => "Este formulário ainda não está disponível.", 7 | 'form_too_late' => "Este formulário já não está mais disponível.", 8 | 9 | 'notifications' => [ 10 | 'new_answer' => [ 11 | 'subject' => 'Nova resposta para o formulário :form', 12 | 'greeting' => 'Resposta de :date', 13 | ], 14 | 'daily_answers' => [ 15 | 'subject' => 'Respostas do dia, do formulário :form', 16 | 'greeting' => ':count resposta(s)', 17 | ] 18 | ] 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/pt-BR/sharp.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'form' => "Formulário", 10 | 'section' => "Seção", 11 | 'field' => "Campo", 12 | 'answer' => "Resposta", 13 | ], 14 | 15 | 'forms' => [ 16 | 'no_title' => "(sem título)", 17 | 'notification_strategies' => [ 18 | Form::NOTIFICATION_STRATEGY_EVERY => "A cada resposta", 19 | Form::NOTIFICATION_STRATEGY_GROUPED => "Uma vez por dia", 20 | Form::NOTIFICATION_STRATEGY_NONE => "Nunca", 21 | ], 22 | 'fields' => [ 23 | "title" => [ 24 | "label" => "Título" 25 | ], 26 | "is_title_hidden" => [ 27 | "label" => "Não mostrar o título" 28 | ], 29 | "description" => [ 30 | "label" => "Descrição" 31 | ], 32 | "published_at" => [ 33 | "label" => "De" 34 | ], 35 | "unpublished_at" => [ 36 | "label" => "até" 37 | ], 38 | "success_message" => [ 39 | "label" => "Mensagem mostrada após responder o formulário", 40 | "help_text" => "Este texto será mostrado ao usuário quando ele submeter sua resposta. Caso deixada em branco, uma mensagem padrão será usada.", 41 | ], 42 | "notifications_strategy" => [ 43 | "label" => "Frequência de envio ao administrador" 44 | ], 45 | "administrator_email" => [ 46 | "label" => "E-mail do administrador" 47 | ], 48 | "fieldsets" => [ 49 | "title" => "Título do formulário", 50 | "dates" => "Data de disponibilização (opcional)", 51 | "notifications" => "Notificações", 52 | ], 53 | ], 54 | "list" => [ 55 | "columns" => [ 56 | "ref_label" => "ID", 57 | "title_label" => "Título", 58 | "description_label" => "Descrição", 59 | "published_at_label" => "datas de disponibilidade", 60 | "sections_label" => "Seções", 61 | "answers_label" => "Respostas", 62 | ], 63 | "data" => [ 64 | "dates" => [ 65 | "both" => "De %s
até %s", 66 | "from" => "De %s", 67 | "to" => "Até %s", 68 | ] 69 | ] 70 | ] 71 | ], 72 | 73 | 'sections' => [ 74 | "list" => [ 75 | "columns" => [ 76 | "title_label" => "Título", 77 | "description_label" => "Descrição", 78 | ], 79 | "data" => [ 80 | "title" => [ 81 | "is_hidden" => "Escondido", 82 | ] 83 | ] 84 | ], 85 | "fields" => [ 86 | "title" => [ 87 | "label" => "Título" 88 | ], 89 | "is_title_hidden" => [ 90 | "label" => "Título escondido" 91 | ], 92 | "description" => [ 93 | "label" => "Descrição" 94 | ], 95 | "fields" => [ 96 | "label" => "Campos" 97 | ] 98 | ], 99 | ], 100 | 101 | 'fields' => [ 102 | 'types' => [ 103 | Field::TYPE_TEXT => "Texto simples", 104 | Field::TYPE_TEXTAREA => "Texto com várias linhas", 105 | Field::TYPE_SELECT => "Lista de seleção (dropdown)", 106 | Field::TYPE_HEADING => "Subtítulo", 107 | Field::TYPE_UPLOAD => "Arquivo", 108 | ], 109 | 'fields' => [ 110 | "label" => [ 111 | "label" => "Pergunta" 112 | ], 113 | "identifier" => [ 114 | "label" => "Identificador único", 115 | "help_text" => "Campo técnico, precisa ser único para todo o formulário (não é mostrado ao usuário). Utilize o separador _ (exemplo: time_do_coracao)" 116 | ], 117 | "type" => [ 118 | "label" => "" 119 | ], 120 | "help_text" => [ 121 | "label" => "Texto de ajuda" 122 | ], 123 | "required" => [ 124 | "text" => "Campo obrigatório" 125 | ], 126 | "max_length" => [ 127 | "label" => "Tamanho máximo", 128 | "help_text" => "Em número de caracteres", 129 | ], 130 | "rows_count" => [ 131 | "label" => "Número de linhas", 132 | ], 133 | "multiple" => [ 134 | "text" => "Permitir múltipla escolha" 135 | ], 136 | "radios" => [ 137 | "text" => "Mostrar como botões radio" 138 | ], 139 | "max_options" => [ 140 | "label" => "Número máximo de escolhas" 141 | ], 142 | "options" => [ 143 | "label" => "Valores possíveis", 144 | "add_label" => "Adicionar um valor", 145 | ], 146 | "max_size" => [ 147 | "label" => "Tamanho máximo", 148 | "help_text" => "Inteiro, em MegaBytes.", 149 | ], 150 | "accept" => [ 151 | "label" => "Extensões permitidas (opcional)", 152 | "help_text" => "Lista de extensões separadas por vírgula, sem espaços.", 153 | ], 154 | "fieldsets" => [ 155 | "identifiers" => "Identificadores", 156 | ], 157 | ], 158 | "list" => [ 159 | "columns" => [ 160 | "type_label" => "", 161 | "label_label" => "Pergunta", 162 | "help_text_label" => "Texto de ajuda", 163 | ], 164 | "data" => [ 165 | "label" => [ 166 | "required" => "obrigatório" 167 | ] 168 | ] 169 | ] 170 | ], 171 | 172 | 'answers' => [ 173 | "list" => [ 174 | "columns" => [ 175 | "created_at_label" => "Data", 176 | "content_label" => "", 177 | ], 178 | "data" => [ 179 | "label" => [ 180 | "required" => "obrigatório" 181 | ] 182 | ] 183 | ], 184 | 'fields' => [ 185 | 'replies' => [ 186 | 'label' => 'Conteúdo' 187 | ] 188 | ], 189 | 'commands' => [ 190 | 'export' => "Exportar respostas (XLS)", 191 | 'download_files' => "Download dos anexos da resposta", 192 | ], 193 | 'errors' => [ 194 | 'no_file_to_download' => "Esta resposta não contém nenhum arquivo anexo.", 195 | ] 196 | ], 197 | 198 | 'replies' => [ 199 | "list" => [ 200 | "columns" => [ 201 | "label_label" => "Campo", 202 | "value_label" => "Valor", 203 | ], 204 | ], 205 | ] 206 | 207 | ]; 208 | -------------------------------------------------------------------------------- /src/Console/SendFormojNotificationsForYesterday.php: -------------------------------------------------------------------------------- 1 | handle(today()->subDay()); 16 | 17 | $this->info("Finished."); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Controllers/FormojAnswerController.php: -------------------------------------------------------------------------------- 1 | isNotPublishedYet()) { 13 | abort(409, trans("formoj::form.form_too_soon")); 14 | } 15 | 16 | if($form->isNoMorePublished()) { 17 | abort(409, trans("formoj::form.form_too_late")); 18 | } 19 | 20 | return new FormResource($form); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Controllers/FormojFormFillController.php: -------------------------------------------------------------------------------- 1 | storeNewAnswer($request->all()); 16 | 17 | $this->moveFormUploads($form, $request->all(), $answer); 18 | 19 | return response()->json([ 20 | "answer_id" => $answer->id, 21 | "message" => $form->success_message 22 | ? Str::markdown($form->success_message) 23 | : trans("formoj::form.success_message") 24 | ]); 25 | } 26 | 27 | public function update(Form $form, Answer $answer, FormRequest $request) 28 | { 29 | $answer->fillWithData($request->all())->save(); 30 | 31 | $this->moveFormUploads($form, $request->all(), $answer); 32 | 33 | return response()->json([ 34 | "answer_id" => $answer->id, 35 | "message" => $form->success_message 36 | ? Str::markdown($form->success_message) 37 | : trans("formoj::form.success_message") 38 | ]); 39 | } 40 | 41 | protected function moveFormUploads(Form $form, array $data, Answer $answer) 42 | { 43 | collect($data) 44 | ->filter(function ($value, $key) use ($form) { 45 | if(!is_array($value)) { 46 | return false; 47 | } 48 | 49 | if(!($value["uploaded"] ?? false)) { 50 | return false; 51 | } 52 | 53 | $field = $form->findField(substr($key, 1)); 54 | 55 | return $field && $field->isTypeUpload(); 56 | }) 57 | ->each(function ($value) use ($form, $answer) { 58 | $filename = $value["file"]; 59 | 60 | Storage::disk(config('formoj.storage.disk')) 61 | ->writeStream( 62 | config('formoj.storage.path') . "/{$form->id}/answers/{$answer->id}/$filename", 63 | Storage::disk(config('formoj.upload.disk')) 64 | ->readStream(config('formoj.upload.path') . "/{$form->id}/$filename") 65 | ); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Controllers/FormojSectionController.php: -------------------------------------------------------------------------------- 1 | json(["ok" => true]); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Controllers/FormojUploadController.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 17 | } 18 | 19 | public function store(Form $form, Field $field, UploadRequest $request) 20 | { 21 | $path = $request 22 | ->file('file') 23 | ->storeAs( 24 | config("formoj.upload.path") . "/{$form->id}", 25 | $this->getStoreFileName($request->file, config("formoj.upload.path") . "/{$form->id}"), 26 | config("formoj.upload.disk") 27 | ); 28 | 29 | return response()->json([ 30 | "file" => basename($path), 31 | "uploaded" => true 32 | ]); 33 | } 34 | 35 | protected function getStoreFileName($file, $folder): string 36 | { 37 | $filename = $file->getClientOriginalName(); 38 | $disk = $this->fileSystem->disk(config("formoj.upload.disk")); 39 | 40 | for($k=1; $disk->exists("/$folder/$filename"); $k++) { 41 | $filename = explode(".", $file->getClientOriginalName())[0] 42 | . "-{$k}." 43 | . $file->getClientOriginalExtension(); 44 | } 45 | 46 | return $filename; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Controllers/Requests/FormRequest.php: -------------------------------------------------------------------------------- 1 | query("validate_all", 0) 12 | ? $this->form->sections->pluck('fields')->flatten() 13 | : $this->form->sections->last()->fields; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Controllers/Requests/SectionRequest.php: -------------------------------------------------------------------------------- 1 | form->isNotPublishedYet() && !$this->form->isNoMorePublished(); 15 | } 16 | 17 | public function rules(): array 18 | { 19 | return $this 20 | ->currentSectionFields() 21 | ->mapWithKeys(function(Field $field) { 22 | $rules = $field->required ? ["required"] : ["nullable"]; 23 | 24 | if(($field->isTypeText() || $field->isTypeTextarea()) && $field->fieldAttribute("max_length")) { 25 | $rules[] = "max:" . $field->fieldAttribute("max_length"); 26 | } 27 | 28 | if($field->isTypeSelect()) { 29 | $rules[] = Rule::in( 30 | collect($field->fieldAttribute("options")) 31 | ->keys() 32 | ->map(function($index) { 33 | return $index + 1; 34 | }) 35 | ); 36 | 37 | if(!$field->fieldAttribute("radios") && $field->fieldAttribute("multiple")) { 38 | $rules[] = "array"; 39 | 40 | if($field->fieldAttribute("max_options")) { 41 | $rules[] = "max:" . $field->fieldAttribute("max_options"); 42 | } 43 | } 44 | } 45 | 46 | return [ 47 | $field->getFrontId() => $rules 48 | ]; 49 | }) 50 | ->all(); 51 | } 52 | 53 | protected function currentSectionFields(): Collection 54 | { 55 | return $this->section->fields; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Controllers/Requests/UploadRequest.php: -------------------------------------------------------------------------------- 1 | form->isNotPublishedYet() 12 | && !$this->form->isNoMorePublished() 13 | && $this->field->isTypeUpload(); 14 | } 15 | 16 | public function rules(): array 17 | { 18 | return [ 19 | "file" => array_merge( 20 | [ 21 | "file", 22 | "max:" . ($this->field->fieldAttribute("max_size") * 1024) 23 | ], 24 | $this->field->fieldAttribute("accept") 25 | ? ["mimes:" . $this->formatExtensions($this->field->fieldAttribute("accept"))] 26 | : [] 27 | ) 28 | ]; 29 | } 30 | 31 | /** 32 | * @param string $extensions 33 | * @return array 34 | */ 35 | private function formatExtensions($extensions) 36 | { 37 | return collect(explode(",", $extensions)) 38 | ->map(function($extension) { 39 | return substr($extension, 1); 40 | }) 41 | ->implode(","); 42 | } 43 | } -------------------------------------------------------------------------------- /src/FormojServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__.'/api.php'); 14 | 15 | $this->loadMigrationsFrom(dirname(__DIR__) . "/database/migrations"); 16 | 17 | $this->loadTranslationsFrom(dirname(__DIR__) . '/resources/lang', 'formoj'); 18 | 19 | $this->loadViewsFrom(dirname(__DIR__) . '/resources/views', 'formoj'); 20 | 21 | $this->commands([ 22 | SendFormojNotificationsForYesterday::class, 23 | ]); 24 | 25 | $this->publishes([ 26 | dirname(__DIR__) . '/resources/lang' => resource_path('lang/vendor/formoj') 27 | ], 'lang'); 28 | 29 | $this->publishes([ 30 | __DIR__.'/config.php' => config_path('formoj.php'), 31 | ], 'config'); 32 | } 33 | 34 | public function register() 35 | { 36 | $this->mergeConfigFrom( 37 | __DIR__.'/config.php', 'formoj' 38 | ); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Job/ExportAnswersToXls.php: -------------------------------------------------------------------------------- 1 | form = $form; 22 | $this->fileName = $fileName; 23 | $this->answers = $answers ?: $form->answers; 24 | } 25 | 26 | public function handle(Excel $excel) 27 | { 28 | $excel->store( 29 | new AnswersExcelCollection($this->form, $this->answers), 30 | config("formoj.export.path") . "/{$this->fileName}", 31 | config("formoj.export.disk") 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Job/SendDailyNotifications.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d')) 19 | ->orderBy("created_at", "asc") 20 | ->whereIn("form_id", function($query) { 21 | return $query->from("formoj_forms") 22 | ->select("id") 23 | ->where("notifications_strategy", Form::NOTIFICATION_STRATEGY_GROUPED); 24 | }) 25 | ->get() 26 | ->groupBy("form_id") 27 | ->each(function($answers, $formId) { 28 | $form = Form::findOrFail($formId); 29 | 30 | Notification::route('mail', $form->administrator_email) 31 | ->notify(new FormojFormWasAnsweredToday($form, $answers)); 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Job/Utils/AnswersExcelCollection.php: -------------------------------------------------------------------------------- 1 | form = $form; 28 | $this->answers = $answers; 29 | } 30 | 31 | public function headings(): array 32 | { 33 | return array_merge( 34 | ["Date"], 35 | $this->form->sections 36 | ->map(function(Section $section) { 37 | return $section->fields->filter(function(Field $field) { 38 | return !$field->isTypeHeading(); 39 | }); 40 | }) 41 | ->map(function($fields) { 42 | return $fields->pluck("identifier"); 43 | }) 44 | ->flatten() 45 | ->all() 46 | ); 47 | } 48 | 49 | public function columnFormats(): array 50 | { 51 | return [ 52 | 'A' => NumberFormat::FORMAT_DATE_DDMMYYYY, 53 | ]; 54 | } 55 | 56 | public function collection(): Collection 57 | { 58 | $missingAnswers = collect($this->headings()) 59 | ->mapWithKeys(function($key) { 60 | return [$key => ""]; 61 | }) 62 | ->all(); 63 | 64 | return $this->answers 65 | ->map(function (Answer $answer) use($missingAnswers) { 66 | return array_merge( 67 | $missingAnswers, 68 | ["Date" => $answer->created_at], 69 | $answer->content 70 | ); 71 | }); 72 | } 73 | 74 | /** 75 | * Bind value to a cell. 76 | * 77 | * @param Cell $cell Cell to bind value to 78 | * @param mixed $value Value to bind in cell 79 | * 80 | * @return bool 81 | * @throws \PhpOffice\PhpSpreadsheet\Exception 82 | */ 83 | public function bindValue(Cell $cell, $value) 84 | { 85 | if (is_array($value)) { 86 | // Multiselect case 87 | $cell->setValueExplicit(implode("\n", $value), DataType::TYPE_STRING); 88 | 89 | return true; 90 | } 91 | 92 | return parent::bindValue($cell, $value); 93 | } 94 | } -------------------------------------------------------------------------------- /src/Models/Answer.php: -------------------------------------------------------------------------------- 1 | 'json', 20 | ]; 21 | 22 | protected static function newFactory() 23 | { 24 | return new AnswerFactory(); 25 | } 26 | 27 | public function form(): BelongsTo 28 | { 29 | return $this->belongsTo(Form::class); 30 | } 31 | 32 | public function content(string $attribute): ?string 33 | { 34 | return $this->content[$attribute] ?? null; 35 | } 36 | 37 | public function getRelatedFields(): Collection 38 | { 39 | return Field::whereIn('identifier', collect($this->content)->keys()) 40 | ->whereHas("section", function(Builder $query) { 41 | return $query->where("form_id", $this->form_id); 42 | }) 43 | ->get(); 44 | } 45 | 46 | public function fillWithData(array $data): self 47 | { 48 | $this->content = collect($data) 49 | // Map to fields 50 | ->map(function($value, $id) { 51 | return [ 52 | "field" => $this->form->findField(substr($id, 1)), 53 | "value" => $value 54 | ]; 55 | }) 56 | 57 | // Filter out unexpected fields 58 | ->filter(function($fieldAndValue) { 59 | return !is_null($fieldAndValue["field"]) 60 | && !$fieldAndValue["field"]->isTypeHeading(); 61 | }) 62 | 63 | // Extract value (select and upload cases) 64 | ->mapWithKeys(function($fieldAndValue) { 65 | $value = $fieldAndValue["value"]; 66 | $field = $fieldAndValue["field"]; 67 | 68 | if($field->isTypeSelect()) { 69 | if($field->fieldAttribute("multiple")) { 70 | $value = collect($value) 71 | ->map(function($value) use($field) { 72 | return $field->fieldAttribute("options")[$value - 1] ?? null; 73 | }) 74 | ->filter(function($value) { 75 | return !is_null($value); 76 | }) 77 | ->all(); 78 | 79 | } else { 80 | $value = $field->fieldAttribute("options")[$value - 1] ?? ''; 81 | } 82 | 83 | } elseif($field->isTypeUpload()) { 84 | $value = $value['file'] ?? null; 85 | } 86 | 87 | return [$field->identifier => $value]; 88 | }) 89 | ->toArray(); 90 | 91 | return $this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Models/Creators/FieldCreator.php: -------------------------------------------------------------------------------- 1 | label = $label; 18 | $this->section = $section; 19 | } 20 | 21 | public function setRequired(bool $required = true): self 22 | { 23 | $this->required = $required; 24 | 25 | return $this; 26 | } 27 | 28 | public function setHelpText(string $helpText): self 29 | { 30 | $this->helpText = $helpText; 31 | 32 | return $this; 33 | } 34 | 35 | public function create(): Field 36 | { 37 | return $this->section 38 | ->fields() 39 | ->create([ 40 | "help_text" => $this->helpText, 41 | "label" => $this->label, 42 | "required" => $this->required, 43 | "type" => $this->getType(), 44 | "field_attributes" => $this->getFieldAttributes() 45 | ]); 46 | } 47 | 48 | abstract protected function getType(): string; 49 | 50 | abstract protected function getFieldAttributes(): array; 51 | } -------------------------------------------------------------------------------- /src/Models/Creators/SelectFieldCreator.php: -------------------------------------------------------------------------------- 1 | setOptions($options); 26 | } 27 | 28 | protected function getType(): string 29 | { 30 | return Field::TYPE_SELECT; 31 | } 32 | 33 | /** 34 | * @param array|Collection $options 35 | * @return $this 36 | */ 37 | public function setOptions($options): self 38 | { 39 | $this->options = $options; 40 | 41 | return $this; 42 | } 43 | 44 | public function setMaxOptions(int $maxOptions): self 45 | { 46 | $this->maxOptions = $maxOptions; 47 | 48 | return $this; 49 | } 50 | 51 | public function setMultiple(bool $multiple = true): self 52 | { 53 | $this->multiple = $multiple; 54 | 55 | return $this; 56 | } 57 | 58 | public function setRadios(bool $radios = true): self 59 | { 60 | $this->radios = $radios; 61 | 62 | return $this; 63 | } 64 | 65 | protected function getFieldAttributes(): array 66 | { 67 | return [ 68 | "options" => (array) $this->options, 69 | "multiple" => $this->multiple && !$this->radios, 70 | "radios" => $this->radios, 71 | "max_options" => $this->maxOptions, 72 | ]; 73 | } 74 | } -------------------------------------------------------------------------------- /src/Models/Creators/TextFieldCreator.php: -------------------------------------------------------------------------------- 1 | maxLength = $maxLength; 19 | 20 | return $this; 21 | } 22 | 23 | protected function getFieldAttributes(): array 24 | { 25 | return [ 26 | "max_length" => $this->maxLength 27 | ]; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Models/Creators/TextareaFieldCreator.php: -------------------------------------------------------------------------------- 1 | $rowsCount = $rowsCount; 19 | 20 | return $this; 21 | } 22 | 23 | protected function getFieldAttributes(): array 24 | { 25 | return [ 26 | "max_length" => $this->maxLength, 27 | "rows_count" => $this->rowsCount, 28 | ]; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Models/Creators/UploadFieldCreator.php: -------------------------------------------------------------------------------- 1 | maxSize = $maxSize; 20 | 21 | return $this; 22 | } 23 | 24 | public function setAccept(array $accept): self 25 | { 26 | $this->accept = $accept; 27 | 28 | return $this; 29 | } 30 | 31 | protected function getFieldAttributes(): array 32 | { 33 | return [ 34 | "max_size" => $this->maxSize, 35 | "accept" => implode(",", $this->accept ?? []) 36 | ]; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Models/Field.php: -------------------------------------------------------------------------------- 1 | 'json', 25 | 'required' => 'boolean', 26 | ]; 27 | 28 | protected static function newFactory() 29 | { 30 | return new FieldFactory(); 31 | } 32 | 33 | public function section(): BelongsTo 34 | { 35 | return $this->belongsTo(Section::class); 36 | } 37 | 38 | /** 39 | * @param string $attribute 40 | * @return string|int|null 41 | */ 42 | public function fieldAttribute(string $attribute) 43 | { 44 | return $this->field_attributes[$attribute] ?? null; 45 | } 46 | 47 | public function isTypeText(): bool 48 | { 49 | return $this->type === static::TYPE_TEXT; 50 | } 51 | 52 | public function isTypeTextarea(): bool 53 | { 54 | return $this->type === static::TYPE_TEXTAREA; 55 | } 56 | 57 | public function isTypeSelect(): bool 58 | { 59 | return $this->type === static::TYPE_SELECT; 60 | } 61 | 62 | public function isTypeHeading(): bool 63 | { 64 | return $this->type === static::TYPE_HEADING; 65 | } 66 | 67 | public function isTypeUpload(): bool 68 | { 69 | return $this->type === static::TYPE_UPLOAD; 70 | } 71 | 72 | public function isTypeRating(): bool 73 | { 74 | return $this->type === static::TYPE_RATING; 75 | } 76 | 77 | 78 | public function getFrontId(): string 79 | { 80 | return "f" . $this->id; 81 | } 82 | 83 | /** 84 | * Retrieve the model for a bound value. 85 | * Transform the "f[id]" sent by the front in "[id]" 86 | * Example: f123 -> 123 87 | * 88 | * @param mixed $value 89 | * @param string|null $field 90 | * @return \Illuminate\Database\Eloquent\Model|null 91 | */ 92 | public function resolveRouteBinding($value, $field = null) 93 | { 94 | return $this->findOrFail(substr($value, 1)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Models/Form.php: -------------------------------------------------------------------------------- 1 | "boolean", 26 | "published_at" => "datetime", 27 | "unpublished_at" => "datetime", 28 | ]; 29 | 30 | protected static function newFactory() 31 | { 32 | return new FormFactory(); 33 | } 34 | 35 | public function sections(): HasMany 36 | { 37 | return $this->hasMany(Section::class) 38 | ->orderBy("order"); 39 | } 40 | 41 | public function answers(): HasMany 42 | { 43 | return $this->hasMany(Answer::class) 44 | ->orderBy("created_at"); 45 | } 46 | 47 | public function isNotPublishedYet(): bool 48 | { 49 | return $this->published_at && $this->published_at->isFuture(); 50 | } 51 | 52 | public function isNoMorePublished(): bool 53 | { 54 | return $this->unpublished_at && $this->unpublished_at->isPast(); 55 | } 56 | 57 | public function getLabel(): string 58 | { 59 | if($this->title) { 60 | return sprintf("%s (#%s)", $this->title, $this->id); 61 | } 62 | 63 | return '#' . $this->id; 64 | } 65 | 66 | public function findField($id): ?Field 67 | { 68 | if($field = Field::find($id)) { 69 | return in_array($field->section_id, $this->sections->pluck("id")->all()) 70 | ? $field 71 | : null; 72 | } 73 | 74 | return null; 75 | } 76 | 77 | public function createSection(string $title, ?string $description = null): Section 78 | { 79 | return $this->sections()->create(compact('title', 'description')); 80 | } 81 | 82 | /** 83 | * @param array|Arrayable $data 84 | * @return Answer 85 | */ 86 | public function storeNewAnswer($data): Answer 87 | { 88 | $answer = new Answer([ 89 | "form_id" => $this->id, 90 | ]); 91 | 92 | $answer->fillWithData($data)->save(); 93 | 94 | return tap($answer, function(Answer $answer) { 95 | if($this->notifications_strategy == self::NOTIFICATION_STRATEGY_EVERY) { 96 | Notification::route('mail', $this->administrator_email) 97 | ->notify(new FormojFormWasJustAnswered($answer)); 98 | } 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Models/Resources/AnswerResource.php: -------------------------------------------------------------------------------- 1 | getRelatedFields(); 17 | 18 | return [ 19 | 'id' => $this->id, 20 | 'content' => $this->content, 21 | 'fields' => collect($this->content) 22 | ->map(function($value, $identifier) use($formFields) { 23 | /** @var Field $field */ 24 | $field = $formFields->firstWhere('identifier', $identifier); 25 | 26 | return $field 27 | ? [ 28 | 'id' => $field->getFrontId(), 29 | 'name' => $identifier, 30 | 'label' => $field->label, 31 | 'type' => $field->type, 32 | ] 33 | : null; 34 | }) 35 | ->filter() 36 | ->values() 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Models/Resources/FieldResource.php: -------------------------------------------------------------------------------- 1 | $this->getFrontId(), 18 | 'type' => $this->type, 19 | 'name' => $this->identifier, 20 | 'identifier' => $this->identifier, 21 | 'label' => $this->when( 22 | !$this->isTypeHeading(), 23 | $this->label 24 | ), 25 | 'content' => $this->when( 26 | $this->isTypeHeading(), 27 | $this->label 28 | ), 29 | 'helpText' => $this->when( 30 | !$this->isTypeHeading(), 31 | $this->help_text ? Str::markdown($this->help_text) : null 32 | ), 33 | 'required' => $this->when( 34 | !$this->isTypeHeading(), 35 | $this->required 36 | ), 37 | 'maxlength' => $this->when( 38 | $this->isTypeText() || $this->isTypeTextarea(), 39 | $this->fieldAttribute('max_length') 40 | ), 41 | 'multiple' => $this->when( 42 | $this->isTypeSelect() && !$this->fieldAttribute('radios'), 43 | $this->fieldAttribute('multiple') 44 | ), 45 | 'radios' => $this->when( 46 | $this->isTypeSelect(), 47 | $this->fieldAttribute('radios') 48 | ), 49 | 'max' => $this->when( 50 | $this->isTypeSelect() && $this->fieldAttribute('multiple') && !$this->fieldAttribute('radios'), 51 | $this->fieldAttribute('max_options') 52 | ), 53 | 'rows' => $this->when( 54 | $this->isTypeTextArea(), 55 | $this->fieldAttribute('rows_count') 56 | ), 57 | 'maxSize' => $this->when( 58 | $this->isTypeUpload(), 59 | $this->fieldAttribute('max_size') 60 | ), 61 | 'accept' => $this->when( 62 | $this->isTypeUpload(), 63 | $this->fieldAttribute('accept') 64 | ), 65 | 'options' => $this->when( 66 | $this->isTypeSelect(), 67 | collect($this->fieldAttribute('options'))->map( 68 | function($value, $index) { 69 | return [ 70 | "id" => $index+1, 71 | "label" => $value 72 | ]; 73 | } 74 | ) 75 | ), 76 | "lowestLabel" => $this->when( 77 | $this->isTypeRating(), 78 | $this->fieldAttribute('lowest_label') 79 | ), 80 | "highestLabel" => $this->when( 81 | $this->isTypeRating(), 82 | $this->fieldAttribute('highest_label') 83 | ), 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Models/Resources/FormResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 14 | 'title' => $this->title, 15 | 'isTitleHidden' => $this->is_title_hidden, 16 | 'description' => $this->description 17 | ? Str::markdown($this->description) 18 | : null, 19 | 'sections' => SectionResource::collection($this->sections) 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Models/Resources/SectionResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'title' => $this->title, 14 | 'isTitleHidden' => $this->is_title_hidden, 15 | 'description' => $this->description, 16 | 'fields' => FieldResource::collection($this->fields) 17 | ]; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Models/Section.php: -------------------------------------------------------------------------------- 1 | "boolean", 23 | ]; 24 | 25 | protected static function newFactory() 26 | { 27 | return new SectionFactory(); 28 | } 29 | 30 | public function form(): BelongsTo 31 | { 32 | return $this->belongsTo(Form::class); 33 | } 34 | 35 | public function fields(): HasMany 36 | { 37 | return $this->hasMany(Field::class) 38 | ->orderBy("order"); 39 | } 40 | 41 | public function newTextField(string $label): TextFieldCreator 42 | { 43 | return new TextFieldCreator($this, $label); 44 | } 45 | 46 | public function newTextareaField(string $label): TextareaFieldCreator 47 | { 48 | return new TextareaFieldCreator($this, $label); 49 | } 50 | 51 | /** 52 | * @param string $label 53 | * @param array|Arrayable $options 54 | * @return SelectFieldCreator 55 | */ 56 | public function newSelectField(string $label, $options): SelectFieldCreator 57 | { 58 | return new SelectFieldCreator($this, $label, $options); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Notifications/FormojFormWasAnsweredToday.php: -------------------------------------------------------------------------------- 1 | form = $form; 22 | $this->answers = $answers; 23 | } 24 | 25 | public function via($notifiable) 26 | { 27 | return ['mail']; 28 | } 29 | 30 | public function toMail($notifiable) 31 | { 32 | $message = (new MailMessage) 33 | ->subject( 34 | trans('formoj::form.notifications.daily_answers.subject', [ 35 | 'form' => $this->form->getLabel() 36 | ]) 37 | ) 38 | ->greeting(trans('formoj::form.notifications.daily_answers.greeting', [ 39 | 'count' => count($this->answers) 40 | ])); 41 | 42 | foreach($this->answers->take(3) as $answer) { 43 | foreach ($answer->content as $field => $value) { 44 | if (is_array($value)) { 45 | $value = implode(", ", $value); 46 | } 47 | 48 | $message->line($field . ": " . $value); 49 | } 50 | 51 | $message->line("---"); 52 | } 53 | 54 | if(count($this->answers) > 3) { 55 | $message->line("..."); 56 | } 57 | 58 | return $message; 59 | } 60 | } -------------------------------------------------------------------------------- /src/Notifications/FormojFormWasJustAnswered.php: -------------------------------------------------------------------------------- 1 | answer = $answer; 20 | } 21 | 22 | public function via($notifiable) 23 | { 24 | return ['mail']; 25 | } 26 | 27 | public function toMail($notifiable) 28 | { 29 | $message = (new MailMessage) 30 | ->subject( 31 | trans('formoj::form.notifications.new_answer.subject', [ 32 | 'form' => $this->answer->form->getLabel() 33 | ]) 34 | ) 35 | ->greeting(trans('formoj::form.notifications.new_answer.greeting', [ 36 | 'date' => $this->answer->created_at->isoFormat("LLLL") 37 | ])); 38 | 39 | foreach($this->answer->content as $field => $value) { 40 | if (is_array($value)) { 41 | $value = implode(", ", $value); 42 | } 43 | 44 | $message->line($field . ": " . $value); 45 | } 46 | 47 | return $message; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Sharp/Commands/FormojAnswerDownloadFilesCommand.php: -------------------------------------------------------------------------------- 1 | findOrFail($instanceId); 24 | 25 | $uploadFields = $answer->form->sections 26 | ->map(function(Section $section) { 27 | return $section->fields; 28 | }) 29 | ->flatten() 30 | ->filter->isTypeUpload(); 31 | 32 | if(!$uploadFields->count()) { 33 | throw new SharpApplicativeException(trans("formoj::sharp.answers.errors.no_file_to_download")); 34 | } 35 | 36 | if($uploadFields->count() == 1) { 37 | // Only one field to download 38 | if(!$filename = $answer->content($uploadFields->first()->label)) { 39 | throw new SharpApplicativeException(trans("formoj::sharp.answers.errors.no_file_to_download")); 40 | } 41 | 42 | return $this->download( 43 | config("formoj.storage.path") . "/{$answer->form_id}/answers/{$answer->id}/$filename", 44 | $filename, 45 | config("formoj.storage.disk") 46 | ); 47 | 48 | } else { 49 | // Multiple files: we must create a Zip archive 50 | $archiveName = $this->createArchive($answer); 51 | 52 | return $this->download( 53 | $archiveName, 54 | basename($archiveName), 55 | config("formoj.upload.disk") 56 | ); 57 | } 58 | } 59 | 60 | private function createArchive(Answer $answer): string 61 | { 62 | $archiveFilePath = config("formoj.upload.path") . "/archive-" . $answer->id . ".zip"; 63 | 64 | $zip = new ZipArchive(); 65 | $zip->open( 66 | Storage::disk(config("formoj.upload.disk"))->path($archiveFilePath), 67 | ZipArchive::CREATE | ZipArchive::OVERWRITE 68 | ); 69 | 70 | $answerFilesPath = config("formoj.storage.path") . "/{$answer->form_id}/answers/{$answer->id}"; 71 | collect(Storage::disk(config("formoj.storage.disk"))->files($answerFilesPath)) 72 | ->each(function($file) use($zip) { 73 | $zip->addFile( 74 | Storage::disk(config("formoj.storage.disk"))->path($file), 75 | basename($file) 76 | ); 77 | }); 78 | 79 | $zip->close(); 80 | 81 | return $archiveFilePath; 82 | } 83 | } -------------------------------------------------------------------------------- /src/Sharp/Commands/FormojAnswerExportCommand.php: -------------------------------------------------------------------------------- 1 | excel = $excel; 17 | } 18 | 19 | public function label(): string 20 | { 21 | return trans("formoj::sharp.answers.commands.export"); 22 | } 23 | 24 | public function execute(array $data = []): array 25 | { 26 | $formId = $this->queryParams->filterFor("formoj_form"); 27 | $fileName = uniqid('export') . ".xls"; 28 | 29 | ExportAnswersToXls::dispatch( 30 | Form::findOrFail($formId), 31 | $fileName 32 | ); 33 | 34 | return $this->download( 35 | config("formoj.export.path") . "/{$fileName}", 36 | "form-export.xls", 37 | config("formoj.export.disk") 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Sharp/Entities/FormojAnswerEntity.php: -------------------------------------------------------------------------------- 1 | addField( 20 | EntityListField::make("created_at") 21 | ->setLabel(trans("formoj::sharp.answers.list.columns.created_at_label")) 22 | ) 23 | ->addField( 24 | EntityListField::make("content") 25 | ->setLabel(trans("formoj::sharp.answers.list.columns.content_label")) 26 | ); 27 | } 28 | 29 | function getEntityCommands(): ?array 30 | { 31 | return [ 32 | FormojAnswerExportCommand::class 33 | ]; 34 | } 35 | 36 | protected function getFilters(): ?array 37 | { 38 | return [ 39 | HiddenFilter::make('formoj_form') 40 | ]; 41 | } 42 | 43 | public function getListData(): array|Arrayable 44 | { 45 | $answers = Answer::orderBy("created_at", "desc") 46 | ->where("form_id", $this->queryParams->filterFor("formoj_form")); 47 | 48 | return $this 49 | ->setCustomTransformer("created_at", function($value, $instance) { 50 | return $instance->created_at->isoFormat("LLLL"); 51 | }) 52 | ->setCustomTransformer("content", function($value, $instance) { 53 | return collect($value) 54 | ->map(function($value, $field) { 55 | return $field . ": " 56 | . (is_array($value) ? implode(", ", $value) : $value); 57 | }) 58 | ->take(3) 59 | ->implode("
"); 60 | }) 61 | ->transform($answers->paginate(40)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Sharp/FormojAnswerSharpShow.php: -------------------------------------------------------------------------------- 1 | addField( 22 | SharpShowTextField::make("created_at") 23 | ->setLabel(trans("formoj::sharp.answers.list.columns.created_at_label")) 24 | ) 25 | // This field IS NOT displayed, and is used to handle the custom file DL in the FormojReplySharpEntityList list 26 | ->addField( 27 | SharpShowFileField::make("file") 28 | ) 29 | ->addField( 30 | SharpShowEntityListField::make("replies", "formoj_reply") 31 | ->setLabel(trans("formoj::sharp.answers.fields.replies.label")) 32 | ->hideFilterWithValue("formoj_answer", fn($id) => $id) 33 | ); 34 | } 35 | 36 | protected function buildShowLayout(ShowLayout $showLayout): void 37 | { 38 | $showLayout 39 | ->addSection('', function(ShowLayoutSection $section) { 40 | $section 41 | ->addColumn(6, function(ShowLayoutColumn $column) { 42 | $column 43 | ->withField("created_at"); 44 | }); 45 | }) 46 | ->addEntityListSection("replies"); 47 | } 48 | 49 | public function buildShowConfig(): void 50 | { 51 | $this->configurePageTitleAttribute("page_title"); 52 | } 53 | 54 | function getInstanceCommands(): ?array 55 | { 56 | return [ 57 | FormojAnswerDownloadFilesCommand::class, 58 | ]; 59 | } 60 | 61 | function find($id): array 62 | { 63 | return $this 64 | ->setCustomTransformer('page_title', fn() => trans("formoj::sharp.entities.answer")) 65 | ->setCustomTransformer("created_at", function($value, $instance) { 66 | return $instance->created_at->isoFormat("LLLL"); 67 | }) 68 | ->transform( 69 | Answer::findOrFail($id) 70 | ); 71 | } 72 | 73 | public function delete(mixed $id): void 74 | { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Sharp/FormojFieldSharpEntityList.php: -------------------------------------------------------------------------------- 1 | addField( 19 | EntityListField::make("type") 20 | ->setLabel(trans("formoj::sharp.fields.list.columns.type_label")) 21 | ) 22 | ->addField( 23 | EntityListField::make("label") 24 | ->setLabel(trans("formoj::sharp.fields.list.columns.label_label")) 25 | ) 26 | ->addField( 27 | EntityListField::make("help_text") 28 | ->setLabel(trans("formoj::sharp.fields.list.columns.help_text_label")) 29 | ->hideOnSmallScreens() 30 | ); 31 | } 32 | 33 | function buildListConfig(): void 34 | { 35 | $this->configureReorderable( 36 | new SimpleEloquentReorderHandler(Field::class) 37 | ); 38 | } 39 | 40 | protected function getFilters(): ?array 41 | { 42 | return [ 43 | HiddenFilter::make('formoj_section') 44 | ]; 45 | } 46 | 47 | public function getListData(): array|Arrayable 48 | { 49 | $fields = Field::orderBy("order") 50 | ->where("section_id", $this->queryParams->filterFor("formoj_section")); 51 | 52 | return $this 53 | ->setCustomTransformer("label", function($value, $instance) { 54 | if($instance->isTypeHeading()) { 55 | return sprintf( 56 | '
%s
', 57 | $instance->label 58 | ); 59 | } 60 | 61 | return sprintf( 62 | '
%s
%s
%s
', 63 | $instance->label, 64 | $instance->identifier, 65 | $instance->required ? trans("formoj::sharp.fields.list.data.label.required") : "" 66 | ); 67 | }) 68 | ->setCustomTransformer("type", function($value, $instance) { 69 | return static::fieldTypes($value); 70 | }) 71 | ->transform($fields->get()); 72 | } 73 | 74 | public static function fieldTypes(?string $value = null) 75 | { 76 | $types = [ 77 | Field::TYPE_TEXT => trans("formoj::sharp.fields.types." . Field::TYPE_TEXT), 78 | Field::TYPE_TEXTAREA => trans("formoj::sharp.fields.types." . Field::TYPE_TEXTAREA), 79 | Field::TYPE_SELECT => trans("formoj::sharp.fields.types." . Field::TYPE_SELECT), 80 | Field::TYPE_HEADING => trans("formoj::sharp.fields.types." . Field::TYPE_HEADING), 81 | Field::TYPE_UPLOAD => trans("formoj::sharp.fields.types." . Field::TYPE_UPLOAD), 82 | Field::TYPE_RATING => trans("formoj::sharp.fields.types." . Field::TYPE_RATING), 83 | ]; 84 | 85 | return $value ? ($types[$value] ?? null) : $types; 86 | } 87 | 88 | function delete($id): void 89 | { 90 | Field::findOrFail($id)->delete(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Sharp/FormojFieldSharpForm.php: -------------------------------------------------------------------------------- 1 | configureCreateTitle(trans("formoj::sharp.entities.field")) 28 | ->configureEditTitle(trans("formoj::sharp.entities.field")); 29 | } 30 | 31 | function buildFormFields(FieldsContainer $formFields) : void 32 | { 33 | $formFields 34 | ->addField( 35 | SharpFormTextField::make("label") 36 | ->setMaxLength(200) 37 | ->setLabel(trans("formoj::sharp.fields.fields.label.label")) 38 | ) 39 | ->addField( 40 | SharpFormTextField::make("identifier") 41 | ->setMaxLength(100) 42 | ->setLabel(trans("formoj::sharp.fields.fields.identifier.label")) 43 | ->setHelpMessage(trans("formoj::sharp.fields.fields.identifier.help_text")) 44 | ) 45 | ->addField( 46 | SharpFormEditorField::make("help_text") 47 | ->setRenderContentAsMarkdown() 48 | ->setLabel(trans("formoj::sharp.fields.fields.help_text.label")) 49 | ->setToolbar([ 50 | SharpFormEditorField::B, SharpFormEditorField::I, 51 | SharpFormEditorField::SEPARATOR, 52 | SharpFormEditorField::A, 53 | ]) 54 | ->setHeight(200) 55 | ->addConditionalDisplay("type", "!" . Field::TYPE_HEADING) 56 | ) 57 | ->addField( 58 | SharpFormCheckField::make("required", trans("formoj::sharp.fields.fields.required.text")) 59 | ->addConditionalDisplay("type", "!" . Field::TYPE_HEADING) 60 | ) 61 | ->addField( 62 | SharpFormSelectField::make("type", FormojFieldSharpEntityList::fieldTypes()) 63 | ->setLabel(trans("formoj::sharp.fields.fields.type.label")) 64 | ->setDisplayAsDropdown() 65 | ) 66 | ->addField( 67 | SharpFormTextField::make("max_length") 68 | ->setLabel(trans("formoj::sharp.fields.fields.max_length.label")) 69 | ->setHelpMessage(trans("formoj::sharp.fields.fields.max_length.help_text")) 70 | ->addConditionalDisplay("type", [Field::TYPE_TEXT, Field::TYPE_TEXTAREA]) 71 | ) 72 | ->addField( 73 | SharpFormTextField::make("rows_count") 74 | ->setLabel(trans("formoj::sharp.fields.fields.rows_count.label")) 75 | ->addConditionalDisplay("type", Field::TYPE_TEXTAREA) 76 | ) 77 | ->addField( 78 | SharpFormCheckField::make("multiple", trans("formoj::sharp.fields.fields.multiple.text")) 79 | ->addConditionalDisplay("type", Field::TYPE_SELECT) 80 | ->addConditionalDisplay("!radios") 81 | ) 82 | ->addField( 83 | SharpFormCheckField::make("radios", trans("formoj::sharp.fields.fields.radios.text")) 84 | ->addConditionalDisplay("type", Field::TYPE_SELECT) 85 | ) 86 | ->addField( 87 | SharpFormTextField::make("max_options") 88 | ->setLabel(trans("formoj::sharp.fields.fields.max_options.label")) 89 | ->addConditionalDisplay("type", Field::TYPE_SELECT) 90 | ->addConditionalDisplay("multiple") 91 | ->addConditionalDisplay("!radios") 92 | ) 93 | ->addField( 94 | SharpFormListField::make("options") 95 | ->setLabel(trans("formoj::sharp.fields.fields.options.label")) 96 | ->setAddable()->setAddText(trans("formoj::sharp.fields.fields.options.add_label")) 97 | ->setRemovable() 98 | ->setSortable() 99 | ->addItemField( 100 | SharpFormTextField::make("label") 101 | ) 102 | ->addConditionalDisplay("type", Field::TYPE_SELECT) 103 | ) 104 | ->addField( 105 | SharpFormTextField::make("max_size") 106 | ->setLabel(trans("formoj::sharp.fields.fields.max_size.label")) 107 | ->setHelpMessage(trans("formoj::sharp.fields.fields.max_size.help_text")) 108 | ->addConditionalDisplay("type", Field::TYPE_UPLOAD) 109 | ) 110 | ->addField( 111 | SharpFormTextField::make("accept") 112 | ->setLabel(trans("formoj::sharp.fields.fields.accept.label")) 113 | ->setPlaceholder("Ex: .jpeg,.gif,.png") 114 | ->setHelpMessage(trans("formoj::sharp.fields.fields.accept.help_text")) 115 | ->addConditionalDisplay("type", Field::TYPE_UPLOAD) 116 | ) 117 | ->addField( 118 | SharpFormTextField::make("lowest_label") 119 | ->setLabel(trans("formoj::sharp.fields.fields.lowest_label.label")) 120 | ->addConditionalDisplay("type", Field::TYPE_RATING) 121 | ) 122 | ->addField( 123 | SharpFormTextField::make("highest_label") 124 | ->setLabel(trans("formoj::sharp.fields.fields.highest_label.label")) 125 | ->addConditionalDisplay("type", Field::TYPE_RATING) 126 | ); 127 | } 128 | 129 | function buildFormLayout(FormLayout $formLayout): void 130 | { 131 | $formLayout 132 | ->addColumn(6, function (FormLayoutColumn $column) { 133 | $column 134 | ->withFieldset(trans("formoj::sharp.fields.fields.fieldsets.identifiers"), function (FormLayoutFieldset $fieldset) { 135 | $fieldset 136 | ->withField("label") 137 | ->withField("identifier"); 138 | }) 139 | ->withField("type") 140 | ->withField("required") 141 | ->withField("help_text"); 142 | 143 | }) 144 | ->addColumn(6, function (FormLayoutColumn $column) { 145 | $column 146 | ->withField("max_size") 147 | ->withField("accept") 148 | ->withField("max_length") 149 | ->withField("rows_count") 150 | ->withListField("options", function(FormLayoutColumn $column) { 151 | $column->withField("label"); 152 | }) 153 | ->withField("radios") 154 | ->withField("multiple") 155 | ->withField("lowest_label") 156 | ->withField("highest_label"); 157 | }); 158 | } 159 | 160 | function find($id): array 161 | { 162 | foreach([ 163 | "max_length", "rows_count", "max_options", "radios", "multiple", "max_size", "accept", "lowest_label", "highest_label"] as $attribute) { 164 | $this->setCustomTransformer($attribute, function($value, $field) use($attribute) { 165 | return $field->fieldAttribute($attribute); 166 | }); 167 | } 168 | 169 | return $this 170 | ->setCustomTransformer("options", function($value, $field) { 171 | return collect($field->fieldAttribute("options")) 172 | ->map(function($option) { 173 | return [ 174 | "id" => uniqid(), 175 | "label" => $option 176 | ]; 177 | }) 178 | ->values(); 179 | }) 180 | ->transform(Field::findOrFail($id)); 181 | } 182 | 183 | function update($id, array $data) 184 | { 185 | $field = $id 186 | ? Field::findOrFail($id) 187 | : new Field([ 188 | "section_id" => sharp()->context()->breadcrumb()->previousShowSegment()->instanceId(), 189 | "order" => 100 190 | ]); 191 | 192 | $data["field_attributes"] = []; 193 | 194 | if($data["type"] == Field::TYPE_TEXT) { 195 | $this->transformAttributesToFieldAttributes($data, ["max_length" => "int"]); 196 | 197 | } elseif($data["type"] == Field::TYPE_TEXTAREA) { 198 | $this->transformAttributesToFieldAttributes($data, ["max_length" => "int", "rows_count" => "int"]); 199 | 200 | } elseif($data["type"] == Field::TYPE_SELECT) { 201 | $this->transformAttributesToFieldAttributes($data, ["max_options" => "int", "multiple" => "boolean", "radios" => "boolean"]); 202 | $data["field_attributes"]["options"] = collect($data["options"])->pluck("label")->all(); 203 | 204 | } elseif($data["type"] == Field::TYPE_UPLOAD) { 205 | $this->transformAttributesToFieldAttributes($data, ["max_size" => "int", "accept" => "string"]); 206 | 207 | } elseif($data["type"] == Field::TYPE_RATING) { 208 | $this->transformAttributesToFieldAttributes($data, ["lowest_label" => "string", "highest_label" => "string"]); 209 | } 210 | 211 | unset( 212 | $data["max_length"], $data["rows_count"], $data["options"], 213 | $data["max_options"], $data["multiple"], $data["radios"], 214 | $data["max_size"], $data["accept"], 215 | $data["lowest_label"], $data["highest_label"], 216 | ); 217 | 218 | $this->save($field, $data); 219 | 220 | return $field->id; 221 | } 222 | 223 | protected function transformAttributesToFieldAttributes(&$data, array $attributeLabels): void 224 | { 225 | collect($data) 226 | ->filter(function ($value, $attribute) use($attributeLabels) { 227 | return isset($attributeLabels[$attribute]); 228 | }) 229 | ->each(function($value, $attribute) use(&$data, $attributeLabels) { 230 | $data["field_attributes"][$attribute] = $this->castValue($value, $attributeLabels[$attribute]); 231 | }); 232 | } 233 | 234 | protected function castValue(?string $value, string $type) 235 | { 236 | if($value === null || strlen($value) == 0) { 237 | return null; 238 | } 239 | 240 | switch($type) { 241 | case "int": 242 | return (int) $value; 243 | case "boolean": 244 | return (boolean) $value; 245 | } 246 | 247 | return $value; 248 | } 249 | 250 | public function rules() 251 | { 252 | return [ 253 | 'label' => [ 254 | 'required', 255 | 'max:200' 256 | ], 257 | 'identifier' => [ 258 | 'required', 259 | 'max:100', 260 | 'alpha_dash', 261 | Rule::unique('formoj_fields', 'identifier') 262 | ->whereIn("section_id", 263 | Section::select("id") 264 | ->where("form_id", 265 | Section::find(sharp()->context()->breadcrumb()->previousShowSegment()->instanceId()) 266 | ->form_id 267 | ) 268 | ->pluck("id") 269 | ->all() 270 | ) 271 | ->ignore(sharp()->context()->instanceId()) 272 | ], 273 | 'type' => 'required', 274 | 'max_length' => 'integer|nullable', 275 | 'max_values' => 'integer|nullable', 276 | 'rows_count' => 'integer|nullable|required_if:type,' . Field::TYPE_TEXTAREA, 277 | 'options' => 'required_if:type,' . Field::TYPE_SELECT, 278 | 'options.*.label' => 'required', 279 | 'max_size' => 'required_if:type,' . Field::TYPE_UPLOAD . '|integer|nullable', 280 | 'accept' => ['nullable','regex:/^(\.[a-z]+,)*(\.[a-z]+)$/'] 281 | ]; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Sharp/FormojFormSharpEntityList.php: -------------------------------------------------------------------------------- 1 | addField( 20 | EntityListField::make("ref") 21 | ->setLabel(trans("formoj::sharp.forms.list.columns.ref_label")) 22 | ->setSortable() 23 | ->setWidth(1) 24 | ->hideOnSmallScreens() 25 | ) 26 | ->addField( 27 | EntityListField::make("title") 28 | ->setLabel(trans("formoj::sharp.forms.list.columns.title_label")) 29 | ->setSortable() 30 | ) 31 | ->addField( 32 | EntityListField::make("description") 33 | ->setLabel(trans("formoj::sharp.forms.list.columns.description_label")) 34 | ->hideOnSmallScreens() 35 | ) 36 | ->addField( 37 | EntityListField::make("published_at") 38 | ->setLabel(trans("formoj::sharp.forms.list.columns.published_at_label")) 39 | ->setSortable() 40 | ) 41 | ->addField( 42 | EntityListField::make("sections") 43 | ->setLabel(trans("formoj::sharp.forms.list.columns.sections_label")) 44 | ->hideOnSmallScreens() 45 | ); 46 | } 47 | 48 | function buildListConfig(): void 49 | { 50 | $this 51 | ->configureDefaultSort("ref", "desc") 52 | ->configureSearchable(); 53 | } 54 | 55 | public function getListData(): array|Arrayable 56 | { 57 | $forms = Form::with("sections") 58 | ->orderBy(static::convertSortedBy($this->queryParams->sortedBy()), $this->queryParams->sortedDir()) 59 | ->when($this->queryParams->hasSearch(), function (Builder $query) { 60 | foreach ($this->queryParams->searchWords() as $word) { 61 | $query->where(function ($query) use ($word) { 62 | $query->orWhere("title", "like", $word) 63 | ->orWhere('id', 'like', $word); 64 | }); 65 | } 66 | }); 67 | 68 | $this->addAdditionalWhereClauses($forms); 69 | 70 | return $this 71 | ->setCustomTransformer("ref", function($value, $instance) { 72 | return "#{$instance->id}"; 73 | }) 74 | ->setCustomTransformer("title", function($value, $instance) { 75 | return $instance->title ?: "" . trans("formoj::sharp.forms.no_title") . ""; 76 | }) 77 | ->setCustomTransformer("published_at", function($value, $instance) { 78 | return static::publicationDates($instance); 79 | }) 80 | ->setCustomTransformer("sections", function($value, $instance) { 81 | return $instance->sections->pluck("title")->implode("
"); 82 | }) 83 | ->transform($forms->paginate(40)); 84 | } 85 | 86 | /** 87 | * @param string|null $value 88 | * @return array|string|null 89 | */ 90 | public static function notificationStrategies(?string $value = null) 91 | { 92 | $types = [ 93 | Form::NOTIFICATION_STRATEGY_NONE => trans("formoj::sharp.forms.notification_strategies." . Form::NOTIFICATION_STRATEGY_NONE), 94 | Form::NOTIFICATION_STRATEGY_GROUPED => trans("formoj::sharp.forms.notification_strategies." . Form::NOTIFICATION_STRATEGY_GROUPED), 95 | Form::NOTIFICATION_STRATEGY_EVERY => trans("formoj::sharp.forms.notification_strategies." . Form::NOTIFICATION_STRATEGY_EVERY), 96 | ]; 97 | 98 | return $value ? ($types[$value] ?? null) : $types; 99 | } 100 | 101 | public static function publicationDates(Form $form): string 102 | { 103 | if($form->published_at) { 104 | if($form->unpublished_at) { 105 | return sprintf( 106 | trans("formoj::sharp.forms.list.data.dates.both"), 107 | $form->published_at->isoFormat("LLL"), 108 | $form->unpublished_at->isoFormat("LLL") 109 | ); 110 | } 111 | return sprintf( 112 | trans("formoj::sharp.forms.list.data.dates.from"), 113 | $form->published_at->isoFormat("LLL") 114 | ); 115 | } 116 | 117 | if($form->unpublished_at) { 118 | return sprintf( 119 | trans("formoj::sharp.forms.list.data.dates.to"), 120 | $form->unpublished_at->isoFormat("LLL") 121 | ); 122 | } 123 | 124 | return ""; 125 | } 126 | 127 | private static function convertSortedBy(string $sortedBy) 128 | { 129 | if(in_array($sortedBy, ["title", "published_at"])) { 130 | return $sortedBy; 131 | } 132 | return "id"; 133 | } 134 | 135 | protected function addAdditionalWhereClauses(Builder &$query): Builder 136 | { 137 | return $query; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Sharp/FormojFormSharpForm.php: -------------------------------------------------------------------------------- 1 | addField( 26 | SharpFormTextField::make("title") 27 | ->setMaxLength(200) 28 | ->setLabel(trans("formoj::sharp.forms.fields.title.label")) 29 | ) 30 | ->addField( 31 | SharpFormCheckField::make("is_title_hidden", trans("formoj::sharp.forms.fields.is_title_hidden.label")) 32 | ) 33 | ->addField( 34 | SharpFormEditorField::make("description") 35 | ->setRenderContentAsMarkdown() 36 | ->setLabel(trans("formoj::sharp.forms.fields.description.label")) 37 | ->setToolbar([ 38 | SharpFormEditorField::B, SharpFormEditorField::I, 39 | SharpFormEditorField::SEPARATOR, 40 | SharpFormEditorField::A, 41 | ]) 42 | ->setHeight(200) 43 | ) 44 | ->addField( 45 | SharpFormEditorField::make("success_message") 46 | ->setRenderContentAsMarkdown() 47 | ->setLabel(trans("formoj::sharp.forms.fields.success_message.label")) 48 | ->setToolbar([ 49 | SharpFormEditorField::B, SharpFormEditorField::I, 50 | SharpFormEditorField::SEPARATOR, 51 | SharpFormEditorField::A, 52 | ]) 53 | ->setHeight(200) 54 | ->setHelpMessage(trans("formoj::sharp.forms.fields.success_message.help_text")) 55 | ) 56 | ->addField( 57 | SharpFormDateField::make("published_at") 58 | ->setLabel(trans("formoj::sharp.forms.fields.published_at.label")) 59 | ->setHasTime(true) 60 | ) 61 | ->addField( 62 | SharpFormDateField::make("unpublished_at") 63 | ->setLabel(trans("formoj::sharp.forms.fields.unpublished_at.label")) 64 | ->setHasTime(true) 65 | ) 66 | ->addField( 67 | SharpFormTextField::make("administrator_email") 68 | ->setLabel(trans("formoj::sharp.forms.fields.administrator_email.label")) 69 | ) 70 | ->addField( 71 | SharpFormSelectField::make("notifications_strategy", FormojFormSharpEntityList::notificationStrategies()) 72 | ->setDisplayAsDropdown() 73 | ->setLabel(trans("formoj::sharp.forms.fields.notifications_strategy.label")) 74 | ); 75 | } 76 | 77 | function buildFormLayout(FormLayout $formLayout): void 78 | { 79 | $formLayout 80 | ->addColumn(6, function (FormLayoutColumn $column) { 81 | $column 82 | ->withFieldset(trans("formoj::sharp.forms.fields.fieldsets.title"), function (FormLayoutFieldset $fieldset) { 83 | $fieldset 84 | ->withField("title") 85 | ->withField("is_title_hidden"); 86 | }) 87 | ->withFieldset(trans("formoj::sharp.forms.fields.fieldsets.dates"), function (FormLayoutFieldset $fieldset) { 88 | $fieldset->withFields("published_at|6", "unpublished_at|6"); 89 | }) 90 | ->withField("description"); 91 | 92 | }) 93 | ->addColumn(6, function (FormLayoutColumn $column) { 94 | $column 95 | ->withFieldset(trans("formoj::sharp.forms.fields.fieldsets.notifications"), function (FormLayoutFieldset $fieldset) { 96 | $fieldset 97 | ->withField("notifications_strategy") 98 | ->withField("administrator_email"); 99 | }) 100 | ->withField("success_message"); 101 | }); 102 | } 103 | 104 | function find($id): array 105 | { 106 | return $this->transform(Form::findOrFail($id)); 107 | } 108 | 109 | function update($id, array $data) 110 | { 111 | $form = $id ? Form::findOrFail($id) : new Form(); 112 | 113 | $this->save($form, $data); 114 | 115 | return $form->id; 116 | } 117 | 118 | public function rules() 119 | { 120 | return [ 121 | 'title' => ['max:200', 'nullable'], 122 | 'published_at' => ['date', 'nullable'], 123 | 'unpublished_at' => ['date', 'after:published_at', 'nullable'], 124 | 'administrator_email' => ['required_unless:notifications_strategy,none', 'email', 'nullable'] 125 | ]; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Sharp/FormojFormSharpShow.php: -------------------------------------------------------------------------------- 1 | addField( 21 | SharpShowTextField::make("ref") 22 | ->setLabel(trans("formoj::sharp.forms.list.columns.ref_label")) 23 | ) 24 | ->addField( 25 | SharpShowTextField::make("title") 26 | ->setLabel(trans("formoj::sharp.forms.list.columns.title_label")) 27 | ) 28 | ->addField( 29 | SharpShowTextField::make("published_at") 30 | ->setLabel(trans("formoj::sharp.forms.list.columns.published_at_label")) 31 | ) 32 | ->addField( 33 | SharpShowTextField::make("notifications_strategy") 34 | ->setLabel(trans("formoj::sharp.forms.fields.notifications_strategy.label")) 35 | ) 36 | ->addField( 37 | SharpShowTextField::make("description") 38 | ->setLabel(trans("formoj::sharp.forms.fields.description.label")) 39 | ) 40 | ->addField( 41 | SharpShowTextField::make("success_message") 42 | ->setLabel(trans("formoj::sharp.forms.fields.success_message.label")) 43 | ) 44 | ->addField( 45 | SharpShowEntityListField::make("sections", "formoj_section") 46 | ->setLabel(trans("formoj::sharp.forms.list.columns.sections_label")) 47 | ->hideFilterWithValue("formoj_form", function($instanceId) { 48 | return $instanceId; 49 | }) 50 | ) 51 | ->addField( 52 | SharpShowEntityListField::make("answers", "formoj_answer") 53 | ->setLabel(trans("formoj::sharp.forms.list.columns.answers_label")) 54 | ->hideFilterWithValue("formoj_form", function($instanceId) { 55 | return $instanceId; 56 | }) 57 | ); 58 | } 59 | 60 | protected function buildShowLayout(ShowLayout $showLayout): void 61 | { 62 | $showLayout 63 | ->addSection('', function(ShowLayoutSection $section) { 64 | $section 65 | ->addColumn(6, function(ShowLayoutColumn $column) { 66 | $column 67 | ->withField("ref") 68 | ->withField("title") 69 | ->withField("published_at") 70 | ->withField("notifications_strategy"); 71 | }) 72 | ->addColumn(6, function(ShowLayoutColumn $column) { 73 | $column 74 | ->withField("description") 75 | ->withField("success_message"); 76 | }); 77 | }) 78 | ->addEntityListSection("sections") 79 | ->addEntityListSection("answers"); 80 | } 81 | 82 | public function buildShowConfig(): void 83 | { 84 | $this 85 | ->configurePageTitleAttribute("page_title") 86 | ->configureBreadcrumbCustomLabelAttribute("breadcrumb"); 87 | } 88 | 89 | function find($id): array 90 | { 91 | return $this 92 | ->setCustomTransformer('page_title', fn() => trans("formoj::sharp.entities.form")) 93 | ->setCustomTransformer("ref", function($value, $form) { 94 | return "#{$form->id}"; 95 | }) 96 | ->setCustomTransformer("breadcrumb", function($value, $form) { 97 | return trans("formoj::sharp.entities.form") . " #{$form->id}"; 98 | }) 99 | ->setCustomTransformer("published_at", function($value, $instance) { 100 | return FormojFormSharpEntityList::publicationDates($instance); 101 | }) 102 | ->setCustomTransformer("notifications_strategy", function($value, $instance) { 103 | $label = FormojFormSharpEntityList::notificationStrategies($value); 104 | if($value != Form::NOTIFICATION_STRATEGY_NONE) { 105 | return sprintf("%s (%s)", $label, $instance->administrator_email); 106 | } 107 | return $label; 108 | }) 109 | ->transform( 110 | Form::findOrFail($id) 111 | ); 112 | } 113 | 114 | public function delete(mixed $id): void 115 | { 116 | Form::findOrFail($id)->delete(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Sharp/FormojReplySharpEntityList.php: -------------------------------------------------------------------------------- 1 | addField( 20 | EntityListField::make("label") 21 | ->setLabel(trans("formoj::sharp.replies.list.columns.label_label")) 22 | ) 23 | ->addField( 24 | EntityListField::make("value") 25 | ->setLabel(trans("formoj::sharp.replies.list.columns.value_label")) 26 | ); 27 | } 28 | 29 | protected function getFilters(): ?array 30 | { 31 | return [ 32 | HiddenFilter::make("formoj_answer") 33 | ]; 34 | } 35 | 36 | public function getListData(): array|Arrayable 37 | { 38 | if(!$answer = Answer::find($this->queryParams->filterFor("formoj_answer"))) { 39 | return []; 40 | } 41 | 42 | return $this 43 | ->setCustomTransformer("label", function($value, $instance) { 44 | return $instance->label; 45 | }) 46 | ->setCustomTransformer("value", function($value, $instance) { 47 | return $instance->value; 48 | }) 49 | ->transform( 50 | collect($answer->content) 51 | ->map(function($value, $label) { 52 | return (object)[ 53 | "id" => uniqid(), 54 | "label" => $label, 55 | "value" => $this->formatValue($value, $label) 56 | ]; 57 | }) 58 | ->values() 59 | ); 60 | } 61 | 62 | private function formatValue($value, $label): string 63 | { 64 | if(is_array($value)) { 65 | return collect($value) 66 | ->map(function($item) { 67 | return "- {$item}"; 68 | }) 69 | ->implode("
"); 70 | } 71 | 72 | if($value !== null && $this->seemsToBeAFile($value)) { 73 | return $this->buildSharpDownloadFileLink($value); 74 | } 75 | 76 | return $value ?: ""; 77 | } 78 | 79 | private function seemsToBeAFile(string $value): bool 80 | { 81 | if(preg_match('/^.+\.[A-Za-z]{3}$/U', $value)) { 82 | if(($formId = $this->getCurrentFormId()) && ($answerId = $this->getCurrentAnswerId())) { 83 | return Storage::disk(config("formoj.storage.disk")) 84 | ->exists( 85 | sprintf( 86 | "%s/%s/answers/%s/%s", 87 | config("formoj.storage.path"), 88 | $formId, 89 | $answerId, 90 | $value 91 | ) 92 | ); 93 | } 94 | 95 | return false; 96 | } 97 | 98 | return false; 99 | } 100 | 101 | protected function getCurrentFormId(): ?string 102 | { 103 | if($formShow = sharp()->context()->breadcrumb()->previousShowSegment("formoj_form")) { 104 | return $formShow->instanceId(); 105 | } 106 | 107 | return null; 108 | } 109 | 110 | protected function getCurrentAnswerId(): ?string 111 | { 112 | if($answerShow = sharp()->context()->breadcrumb()->previousShowSegment("formoj_answer")) { 113 | return $answerShow->instanceId(); 114 | } 115 | 116 | return null; 117 | } 118 | 119 | protected function buildSharpDownloadFileLink(string $fileName): string 120 | { 121 | return sprintf( 122 | '%s', 123 | route("code16.sharp.api.download.show", [ 124 | "entityKey" => "formoj_answer", 125 | "instanceId" => $this->getCurrentAnswerId(), 126 | "path" => sprintf( 127 | "%s/%s/answers/%s/%s", 128 | config("formoj.storage.path"), 129 | $this->getCurrentFormId(), 130 | $this->getCurrentAnswerId(), 131 | $fileName 132 | ), 133 | "disk" => config("formoj.storage.disk"), 134 | ]), 135 | $fileName 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Sharp/FormojSectionSharpEntityList.php: -------------------------------------------------------------------------------- 1 | addField( 19 | EntityListField::make("title") 20 | ->setLabel(trans("formoj::sharp.sections.list.columns.title_label")) 21 | ) 22 | ->addField( 23 | EntityListField::make("description") 24 | ->setLabel(trans("formoj::sharp.sections.list.columns.description_label")) 25 | ); 26 | } 27 | 28 | function buildListConfig(): void 29 | { 30 | $this 31 | ->configureReorderable( 32 | new SimpleEloquentReorderHandler(Section::class) 33 | ); 34 | } 35 | 36 | protected function getFilters(): ?array 37 | { 38 | return [ 39 | HiddenFilter::make('formoj_form') 40 | ]; 41 | } 42 | 43 | public function getListData(): array|Arrayable 44 | { 45 | $sections = Section::orderBy("order") 46 | ->where("form_id", $this->queryParams->filterFor("formoj_form")); 47 | 48 | return $this 49 | ->setCustomTransformer("title", function($value, $instance) { 50 | return static::transformTitle($instance); 51 | }) 52 | ->transform($sections->get()); 53 | } 54 | 55 | public static function transformTitle(Section $section): string 56 | { 57 | return sprintf( 58 | '
%s
%s
', 59 | $section->title, 60 | $section->is_title_hidden 61 | ? trans("formoj::sharp.sections.list.data.title.is_hidden") 62 | : "" 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Sharp/FormojSectionSharpForm.php: -------------------------------------------------------------------------------- 1 | addField( 23 | SharpFormTextField::make("title") 24 | ->setMaxLength(200) 25 | ->setLabel(trans("formoj::sharp.sections.fields.title.label")) 26 | ) 27 | ->addField( 28 | SharpFormCheckField::make("is_title_hidden", trans("formoj::sharp.sections.fields.is_title_hidden.label")) 29 | ) 30 | ->addField( 31 | SharpFormTextareaField::make("description") 32 | ->setLabel(trans("formoj::sharp.sections.fields.description.label")) 33 | ->setRowCount(3) 34 | ); 35 | } 36 | 37 | function buildFormLayout(FormLayout $formLayout): void 38 | { 39 | $formLayout 40 | ->addColumn(6, function (FormLayoutColumn $column) { 41 | $column 42 | ->withField("title") 43 | ->withField("is_title_hidden"); 44 | 45 | }) 46 | ->addColumn(6, function (FormLayoutColumn $column) { 47 | $column 48 | ->withField("description"); 49 | }); 50 | } 51 | 52 | function find($id): array 53 | { 54 | return $this 55 | ->transform(Section::findOrFail($id)); 56 | } 57 | 58 | function update($id, array $data) 59 | { 60 | $form = $id 61 | ? Section::findOrFail($id) 62 | : new Section([ 63 | "form_id" => sharp()->context()->breadcrumb()->previousShowSegment()->instanceId(), 64 | "order" => 100 65 | ]); 66 | 67 | $this->save($form, $data); 68 | 69 | return $form->id; 70 | } 71 | 72 | public function rules() 73 | { 74 | return [ 75 | 'title' => ['required', 'max:200'], 76 | ]; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Sharp/FormojSectionSharpShow.php: -------------------------------------------------------------------------------- 1 | addField( 20 | SharpShowTextField::make("title") 21 | ->setLabel(trans("formoj::sharp.sections.fields.title.label")) 22 | ) 23 | ->addField( 24 | SharpShowTextField::make("description") 25 | ->setLabel(trans("formoj::sharp.sections.fields.description.label")) 26 | ) 27 | ->addField( 28 | SharpShowEntityListField::make("fields", "formoj_field") 29 | ->setLabel(trans("formoj::sharp.sections.fields.fields.label")) 30 | ->hideFilterWithValue("formoj_section", function($instanceId) { 31 | return $instanceId; 32 | }) 33 | ); 34 | } 35 | 36 | protected function buildShowLayout(ShowLayout $showLayout): void 37 | { 38 | $showLayout 39 | ->addSection('', function(ShowLayoutSection $section) { 40 | $section 41 | ->addColumn(6, function(ShowLayoutColumn $column) { 42 | $column 43 | ->withField("title"); 44 | }) 45 | ->addColumn(6, function(ShowLayoutColumn $column) { 46 | $column 47 | ->withField("description"); 48 | });; 49 | }) 50 | ->addEntityListSection("fields"); 51 | } 52 | 53 | public function buildShowConfig(): void 54 | { 55 | $this 56 | ->configurePageTitleAttribute("page_title") 57 | ->configureBreadcrumbCustomLabelAttribute("breadcrumb"); 58 | } 59 | 60 | function find($id): array 61 | { 62 | return $this 63 | ->setCustomTransformer("page_title", fn() => trans("formoj::sharp.entities.section")) 64 | ->setCustomTransformer("breadcrumb", function($value, $instance) { 65 | return $instance->title; 66 | }) 67 | ->setCustomTransformer("title", function($value, $instance) { 68 | return FormojSectionSharpEntityList::transformTitle($instance); 69 | }) 70 | ->transform( 71 | Section::findOrFail($id) 72 | ); 73 | } 74 | 75 | public function delete(mixed $id): void 76 | { 77 | Section::findOrFail($id)->delete(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/api.php: -------------------------------------------------------------------------------- 1 | config("formoj.base_url"), 11 | 'middleware' => config("formoj.api_middleware"), 12 | ], function() { 13 | Route::get('/form/{form}', [FormojFormController::class, 'show']); 14 | Route::post('/form/{form}/validate/{section}', [FormojSectionController::class, 'update']); 15 | Route::post('/form/{form}', [FormojFormFillController::class, 'store']); 16 | Route::post('/form/{form}/answer/{answer}', [FormojFormFillController::class, 'update']); 17 | Route::post('/form/{form}/upload/{field}', [FormojUploadController::class, 'store']); 18 | Route::get('/answer/{answer}', [FormojAnswerController::class, 'show']); 19 | }); 20 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | "/formoj/api/", 9 | 10 | /** 11 | * API middleware 12 | */ 13 | "api_middleware" => [ 14 | \Illuminate\Routing\Middleware\SubstituteBindings::class 15 | ], 16 | 17 | /** 18 | * Disk and base path used for export XLS storage. 19 | */ 20 | "export" => [ 21 | "disk" => "local", 22 | "path" => "/formoj/tmp" 23 | ], 24 | 25 | /** 26 | * Disk and base path used for temporary uploads (upload fields). 27 | */ 28 | "upload" => [ 29 | "disk" => "local", 30 | "path" => "/formoj/tmp" 31 | ], 32 | 33 | /** 34 | * Disk and base path used for uploads storage (upload fields). 35 | */ 36 | "storage" => [ 37 | "disk" => "local", 38 | "path" => "/formoj/forms" 39 | ] 40 | ]; --------------------------------------------------------------------------------