diff --git a/config/voyager-api.php b/config/voyager-api.php index 07b53c7..1b8df9a 100644 --- a/config/voyager-api.php +++ b/config/voyager-api.php @@ -41,7 +41,7 @@ | Filters config |-------------------------------------------------------------------------- | - | Here you can specify voyager datatable filters settings + | Here you can specify voyager api filters settings | */ 'filters' => [ @@ -56,4 +56,15 @@ ] ], ], + + /* + |-------------------------------------------------------------------------- + | Uuid config + |-------------------------------------------------------------------------- + | + | Here you can specify voyager api uuid + | + */ + + 'uuid' => env('VOYAGER_API_UUID', false), ]; diff --git a/src/Console/Commands/GenerateDocsCommand.php b/src/Console/Commands/GenerateDocsCommand.php index 57b5393..c0b04b5 100644 --- a/src/Console/Commands/GenerateDocsCommand.php +++ b/src/Console/Commands/GenerateDocsCommand.php @@ -333,7 +333,7 @@ protected function showOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -389,7 +389,7 @@ protected function editOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -496,7 +496,7 @@ protected function updateOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -556,7 +556,7 @@ protected function singleUpdateOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -639,7 +639,7 @@ protected function deleteOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -695,7 +695,7 @@ protected function restoreOperation($dataType): Operation 'in' => 'path', 'required' => true, 'schema' => [ - 'type' => 'integer', + 'type' => config('voyager-api.uuid', false) ? 'string' : 'integer', ], ]; @@ -932,71 +932,81 @@ protected function getProperty( ]; switch ($row->type) { - case 'text': + case 'checkbox': // code... break; - + case 'color': + // code... + break; + case 'date': + $property['type'] = 'date'; + $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2})/'; + $property['example'] = '2021-05-17'; + break; + case 'file': + $property['format'] = 'binary'; + $property['description'] = 'doc,docx,pdf'; + break; case 'image': $property['format'] = 'binary'; $property['description'] = 'jpg,jpeg,png'; break; - + case 'multiple_images': + // code... + break; + case 'media_picker': + // code... + break; + case 'number': + $property['type'] = 'integer'; + break; case 'password': $property['type'] = 'password'; break; - + case 'radio_btn': + // code... + break; case 'select_dropdown': $property['description'] = json_encode($row->details->options ?? [], JSON_PRETTY_PRINT); $property['enum'] = array_keys((array) $row->details->options ?? []); break; - - case 'hidden': + case 'select_multiple': // code... break; - - case 'number': - $property['type'] = 'integer'; + case 'multiple_checkbox': + // code... break; - - case 'timestamp': - $property['type'] = 'date-time'; - $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})/'; - $property['example'] = '2021-05-17 00:00'; + case 'rich_text_box': + // code... break; - - case 'relationship': - $property['type'] = 'integer'; + case 'code_editor': + // code... break; - - case 'select_multiple': + case 'text': // code... break; - - case 'rich_text_box': + case 'text_area': // code... break; - - case 'date': - $property['type'] = 'date'; - $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2})/'; - $property['example'] = '2021-05-17'; + case 'markdown_editor': + // code... break; - - case 'checkbox': + case 'time': // code... break; - - case 'text_area': + case 'timestamp': + $property['type'] = 'date-time'; + $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})/'; + $property['example'] = '2021-05-17 00:00'; + break; + case 'hidden': // code... break; - - case 'code_editor': + case 'coordinates': // code... break; - - case 'file': - $property['format'] = 'binary'; - $property['description'] = 'doc,docx,pdf'; + case 'relationship': + $property['type'] = config('voyager-api.uuid', false) ? 'string' : 'integer'; break; default: @@ -1051,71 +1061,85 @@ protected function schema( ]; switch ($row->type) { - case 'text': + case 'checkbox': // code... break; - + case 'color': + // code... + break; + case 'date': + $property['type'] = 'date'; + $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2})/'; + $property['example'] = '2021-05-17'; + break; + case 'file': + $property['format'] = 'binary'; + $property['description'] = 'doc,docx,pdf'; + break; case 'image': $property['format'] = 'binary'; $property['description'] = 'jpg,jpeg,png'; break; - + case 'multiple_images': + // code... + break; + case 'media_picker': + // code... + break; + case 'number': + $property['type'] = 'integer'; + break; case 'password': $property['type'] = 'password'; break; - + case 'radio_btn': + // code... + break; case 'select_dropdown': $property['description'] = json_encode($row->details->options ?? [], JSON_PRETTY_PRINT); $property['enum'] = array_keys((array) $row->details->options ?? []); break; - - case 'hidden': + case 'select_multiple': // code... break; - - case 'number': - $property['type'] = 'integer'; + case 'multiple_checkbox': + // code... break; - - case 'timestamp': - $property['type'] = 'date-time'; - $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})/'; - $property['example'] = '2021-05-17 00:00'; + case 'rich_text_box': + // code... break; - - case 'relationship': - $property['type'] = 'integer'; + case 'code_editor': + // code... break; - - case 'select_multiple': + case 'text': // code... break; - - case 'rich_text_box': + case 'text_area': // code... break; - - case 'date': - $property['type'] = 'date'; - $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2})/'; - $property['example'] = '2021-05-17'; + case 'markdown_editor': + // code... break; - - case 'checkbox': + case 'time': // code... break; - - case 'text_area': + case 'timestamp': + $property['type'] = 'date-time'; + $property['pattern'] = '/([0-9]{4})-(?:[0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2})/'; + $property['example'] = '2021-05-17 00:00'; + break; + case 'hidden': // code... break; - - case 'code_editor': + case 'coordinates': // code... break; + case 'relationship': + $property['type'] = config('voyager-api.uuid', false) ? 'string' : 'integer'; + break; - case 'file': - $property['format'] = 'binary'; - $property['description'] = 'doc,docx,pdf'; + case 'text': + // code... break; default: @@ -1146,8 +1170,7 @@ protected function filterParemeters( if ( $row->type === 'relationship' && - $row->details->type !== 'belongsToMany' && - $row->details->type !== 'belongsToJson' + $row->details->type === 'belongsTo' ) { $parameters[@$row->details->column] = $this->filterRowParemeter($row); continue; @@ -1173,11 +1196,49 @@ protected function filterRowParemeter( ]; switch ($row->type) { - case 'text': + case 'checkbox': + $options = [ + '1' => optional(optional($row)->details)->on ?? 'On', + '0' => optional(optional($row)->details)->off ?? 'Off', + ]; + $parameter['name'] = 'filters[' . $row->field . ']'; + $parameter['schema']['type'] = 'string'; + $parameter['schema']['enum'] = array_keys((array) $options); + // $parameter['explode'] = false; + $parameter['description'] = json_encode($options, JSON_PRETTY_PRINT); + break; + case 'color': // code... break; - + case 'date': + $parameter['schema']['type'] = 'string'; + $parameter['schema']['example'] = '2020-01-01,,2021-06-01'; + break; + case 'file': + case 'image': + case 'multiple_images': + case 'media_picker': + $options = [ + '1' => 'Yes', + '0' => 'No', + ]; + $parameter['name'] = 'filters[' . $row->field . ']'; + $parameter['schema']['type'] = 'string'; + $parameter['schema']['enum'] = array_keys((array) $options); + // $parameter['explode'] = false; + $parameter['description'] = json_encode($options, JSON_PRETTY_PRINT); + break; + case 'number': + $parameter['schema']['type'] = 'string'; + $parameter['schema']['example'] = '10,,20'; + break; + case 'password': + // code... + break; + case 'radio_btn': case 'select_dropdown': + case 'select_multiple': + case 'multiple_checkbox': $parameter['name'] = 'filters[' . $row->field . '][]'; $parameter['schema']['type'] = 'array'; $parameter['schema']['items'] = []; @@ -1186,63 +1247,38 @@ protected function filterRowParemeter( // $parameter['explode'] = false; $parameter['description'] = json_encode($row->details->options ?? [], JSON_PRETTY_PRINT); break; - - case 'hidden': + case 'rich_text_box': + case 'code_editor': + case 'text': + case 'text_area': + case 'markdown_editor': // code... break; - - case 'number': - $parameter['schema']['type'] = 'string'; - $parameter['schema']['example'] = '10,,20'; + case 'time': + // code... break; - case 'timestamp': $parameter['schema']['type'] = 'string'; $parameter['schema']['example'] = '2020-01-01 00:00,,2021-06-01 00:00'; break; + case 'hidden': + // code... + break; + case 'coordinates': + // code... + break; case 'relationship': $parameter['name'] = 'filters[' . @$row->field . '][]'; if ( $row->type === 'relationship' && - $row->details->type !== 'belongsToMany' && - $row->details->type !== 'belongsToJson' + $row->details->type === 'belongsTo' ) { $parameter['name'] = 'filters[' . @$row->details->column . '][]'; } $parameter['schema']['type'] = 'array'; $parameter['schema']['items'] = []; - $parameter['schema']['items']['type'] = 'integer'; - break; - - case 'select_multiple': - // code... - break; - - case 'rich_text_box': - // code... - break; - - case 'date': - $parameter['schema']['type'] = 'string'; - $parameter['schema']['example'] = '2020-01-01,,2021-06-01'; - break; - - case 'checkbox': - // code... - break; - - case 'text_area': - // code... - break; - - case 'code_editor': - // code... - break; - - case 'file': - $parameter['schema']['format'] = 'binary'; - $parameter['schema']['description'] = 'doc,docx,pdf'; + $parameter['schema']['items']['type'] = config('voyager-api.uuid', false) ? 'string' : 'integer'; break; default: diff --git a/src/Http/Traits/IndexAction.php b/src/Http/Traits/IndexAction.php index 2db54c2..f6ac4a4 100644 --- a/src/Http/Traits/IndexAction.php +++ b/src/Http/Traits/IndexAction.php @@ -63,7 +63,7 @@ public function index(Request $request) if ($request->get('showSoftDeleted')) { $showSoftDeleted = true; - $query = $query->withTrashed(); + $query->withTrashed(); } } diff --git a/src/Services/Filter.php b/src/Services/Filter.php index fabcb0e..28dd325 100644 --- a/src/Services/Filter.php +++ b/src/Services/Filter.php @@ -50,6 +50,17 @@ public function handle( return; } + if ($dataType->model_name && $row->field == (app($dataType->model_name))->getKeyName()) { + $this->filterByKey( + $query, + $keyword, + $row, + $dataType, + $request + ); + return; + } + if ($row->type == 'select_dropdown' || $row->type == 'radio_btn') { $this->filterSelectDropdown( $query, @@ -106,7 +117,19 @@ protected function filterImage( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. Probably radio check if field is null or not + $query->when($keyword === '1' || $keyword === 'Yes', function ($query) use ($row, $keyword) { + $query->where(function ($query) use ($row, $keyword) { + $query + ->whereNotNull($row->field) + ->where($row->field, '!=', config('voyager.user.default_avatar', 'users/default.png')); + }); + })->when($keyword === '0' || $keyword === 'No', function ($query) use ($row, $keyword) { + $query->where(function ($query) use ($row, $keyword) { + $query + ->whereNull($row->field) + ->orWhere($row->field, config('voyager.user.default_avatar', 'users/default.png')); + }); + }); } /** @@ -121,10 +144,128 @@ protected function filterRelationship( DataRow $row, DataType $dataType, Request $request + ): void { + if (method_exists($this, 'filterRelationship' . Str::studly($row->details->type))) { + $this->{'filterRelationship' . Str::studly($row->details->type)}( + $query, + $keyword, + $row, + $dataType, + $request + ); + return; + } + } + + /** + * Filter belongsTo relationship + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterRelationshipBelongsTo( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request + ): void { + $keywords = explode(',', $keyword); + $query->whereIn($row->details->column, $keywords); + } + + /** + * Filter hasOne relationship + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterRelationshipHasOne( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request + ): void { + // @TODO Not implemented yet. + } + + /** + * Filter hasMany relationship + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterRelationshipHasMany( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request ): void { // @TODO Not implemented yet. } + /** + * Filter belongsToMany relationship + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterRelationshipBelongsToMany( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request + ): void { + $keywords = explode(',', $keyword); + $model = $query->getModel(); + $options = $row->details; + $belongsToManyRelation = $model->belongsToMany($options->model, $options->pivot_table, $options->foreign_pivot_key ?? null, $options->related_pivot_key ?? null, $options->parent_key ?? null, $options->key); + + $query->whereExists(function ($query) use ($model, $belongsToManyRelation, $options, $keywords) { + $query->from($options->pivot_table) + ->whereColumn($options->pivot_table . '.' . $belongsToManyRelation->getForeignPivotKeyName(), $model->getTable() . '.' . $model->getKeyName()) + ->whereIn($belongsToManyRelation->getRelatedPivotKeyName(), $keywords); + }); + } + + /** + * Filter morphTo relationship + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterRelationshipMorphTo( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request + ): void { + $peices = explode(',,', $keyword); + $morphToType = $peices[0] ?? null; + $morphToIdKeyword = $peices[1] ?? null; + $morphToIdKeywords = $morphToIdKeyword ? explode(',', $morphToIdKeyword) : null; + $options = $row->details; + $typeColumn = $options->type_column; + $column = $options->column; + $types = $options->types ?? []; + + $query->when( + $morphToType && in_array($morphToType, collect($types)->pluck('model')->toArray()), + function ($query) use ($typeColumn, $morphToType) { + $query->where($typeColumn, $morphToType); + } + )->when( + $morphToIdKeywords, + function ($query) use ($column, $morphToIdKeywords) { + $query->whereIn($column, $morphToIdKeywords); + } + ); + } + /** * Filter select multiple * @@ -138,7 +279,14 @@ protected function filterSelectMultiple( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $keywords = explode(',', $keyword); + $query->where(function ($query) use ($row, $keywords) { + foreach ($keywords as $keyword) { + $query->orWhere(function ($query) use ($row, $keyword) { + $query->whereJsonContains($row->field . '->' . $keyword, $keyword); + }); + } + }); } /** @@ -154,7 +302,14 @@ protected function filterMultipleCheckbox( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $keywords = explode(',', $keyword); + $query->where(function ($query) use ($row, $keywords) { + foreach ($keywords as $keyword) { + $query->orWhere(function ($query) use ($row, $keyword) { + $query->whereJsonContains($row->field . '->' . $keyword, $keyword); + }); + } + }); } /** @@ -170,7 +325,8 @@ protected function filterSelectDropdown( DataType $dataType, Request $request ): void { - $query->whereIn($row->field, $keyword); + $keywords = explode(',', $keyword); + $query->whereIn($row->field, $keywords); } /** @@ -186,7 +342,33 @@ protected function filterDate( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. Must be range + $keywords = explode(',', $keyword); + $from = $keywords[0] ?? null; + $to = $keywords[1] ?? null; + if ($from) { + $from = safeCarbonParse($from); + } + if ($to) { + $to = safeCarbonParse($to); + } + + if (count($keywords) === 1 && $from && isValidCarbon($keyword)) { + $query->whereDate($row->field, $from->format('Y-m-d')); + return; + } + + if (count($keywords) === 2) { + $query->when($from && $to, function ($query) use ($row, $from, $to) { + $query->whereBetween($row->field, [$from->format('Y-m-d H:i'), $to->format('Y-m-d H:i')]); + }, function ($query) use ($row, $from, $to) { + $query->when($from, function ($query) use ($row, $from) { + $query->where($row->field, '>=', $from->format('Y-m-d H:i')); + })->when($to, function ($query) use ($row, $to) { + $query->where($row->field, '<=', $to->format('Y-m-d H:i')); + }); + }); + return; + } } /** @@ -218,7 +400,12 @@ protected function filterCheckbox( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $options = $row->details; + $query->when($keyword === '1' || $keyword === 'Yes', function ($query) use ($row, $options) { + $query->where($row->field, '1')->whereNotNull($row->field); + })->when($keyword === '0' || $keyword === 'No', function ($query) use ($row, $options) { + $query->where($row->field, '0')->orWhereNull($row->field); + }); } /** @@ -272,7 +459,27 @@ protected function filterNumber( DataType $dataType, Request $request ): void { - // @TODO must check range + $keywords = explode(',', $keyword); + $from = $keywords[0] ?? null; + $to = $keywords[1] ?? null; + + if (count($keywords) === 1 && $from) { + $query->where($row->field, $from); + return; + } + + if (count($keywords) === 2) { + $query->when($from && $to, function ($query) use ($row, $from, $to) { + $query->whereBetween($row->field, [$from, $to]); + }, function ($query) use ($row, $from, $to) { + $query->when($from, function ($query) use ($row, $from) { + $query->where($row->field, '>=', $from); + })->when($to, function ($query) use ($row, $to) { + $query->where($row->field, '<=', $to); + }); + }); + return; + } } /** @@ -310,7 +517,19 @@ protected function filterFile( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $query->when($keyword === '1' || $keyword === 'Yes', function ($query) use ($row) { + $query->where(function ($query) use ($row) { + $query + ->whereNotNull($row->field) + ->whereJsonLength($row->field, '<>', 0); + }); + })->when($keyword === '0' || $keyword === 'No', function ($query) use ($row) { + $query->where(function ($query) use ($row) { + $query + ->whereNull($row->field) + ->orWhereJsonLength($row->field, 0); + }); + }); } /** @@ -364,7 +583,11 @@ protected function filterMultipleImages( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $query->when($keyword === '1' || $keyword === 'Yes', function ($query) use ($row) { + $query->whereNotNull($row->field); + })->when($keyword === '0' || $keyword === 'No', function ($query) use ($row) { + $query->whereNull($row->field); + }); } /** @@ -380,7 +603,19 @@ protected function filterMediaPicker( DataType $dataType, Request $request ): void { - // @TODO Not implemented yet. + $query->when($keyword === '1' || $keyword === 'Yes', function ($query) use ($row, $keyword) { + $query->where(function ($query) use ($row, $keyword) { + $query + ->whereNotNull($row->field) + ->whereJsonLength($row->field, '<>', 0); + }); + })->when($keyword === '0' || $keyword === 'No', function ($query) use ($row, $keyword) { + $query->where(function ($query) use ($row, $keyword) { + $query + ->whereNull($row->field) + ->orWhereJsonLength($row->field, 0); + }); + }); } /** @@ -425,7 +660,34 @@ protected function filterDefaultTranslated( return; } - // @FIXME if needs to filter in translations as well - $query->where($row->field, 'LIKE', '%' . $keyword . '%'); + $query->whereTranslation($row->field, 'LIKE', '%' . $keyword . '%'); + } + + /** + * Filter by key + * + * @param Builder|QueryBuilder $query Query + * @param mixed $keyword Keyword + */ + protected function filterByKey( + $query, + $keyword, + DataRow $row, + DataType $dataType, + Request $request + ) { + $model = app($dataType->model_name); + switch ($model->getKeyType()) { + case 'int': + $query->whereKey((int) $keyword); + break; + case 'string': + $query->whereKey($keyword); + break; + + default: + // code... + break; + } } }