diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 8b14c47..2007278 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -28,7 +28,7 @@ class TableController extends Controller ]); /** @var Table $tableModel */ - $tableModel = $user->tables()->withCount(['favourites', 'forks', 'revisions', 'comments', 'proposals']) + $tableModel = $user->tables()->withCount(['favourites', 'forks', 'revisions', 'comments']) ->where('name', $table)->first(); if ($tableModel === null) abort(404, "No such table."); @@ -50,6 +50,7 @@ class TableController extends Controller return view('table.view', [ 'table' => $tableModel, 'revision' => $revision, + 'proposals_count' => $tableModel->proposals()->unmerged($tableModel)->count(), 'columns' => $columns, 'rows' => $rows, ]); diff --git a/app/Http/Controllers/TableEditController.php b/app/Http/Controllers/TableEditController.php index a567268..8fcb60f 100644 --- a/app/Http/Controllers/TableEditController.php +++ b/app/Http/Controllers/TableEditController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; +use App\Models\Proposal; use App\Models\Table; use App\Models\User; use App\Tables\Changeset; @@ -351,6 +352,17 @@ class TableEditController extends Controller return back(); } - // + $proposal = Proposal::fromChangeset($changeset); + $proposal->saveOrFail(); + + if (\user()->ownsTable($tableModel)) { + $proposal->createRevision(); + } else { + // TODO send a notification to the table owner + } + + session()->forget($tableModel->draftSessionKey); + + return redirect($tableModel->viewRoute); } } diff --git a/app/Models/Proposal.php b/app/Models/Proposal.php index d3f4db1..715724a 100644 --- a/app/Models/Proposal.php +++ b/app/Models/Proposal.php @@ -4,6 +4,9 @@ namespace App\Models; use App\Models\Concerns\Reportable; use App\Tables\Changeset; +use App\Tables\Column; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\DB; use MightyPork\Exceptions\NotApplicableException; /** @@ -16,7 +19,7 @@ use MightyPork\Exceptions\NotApplicableException; * @property int $revision_id * @property int $author_id * @property string $note - * @property Proposal $changes - JSONB + * @property Changeset $changeset - JSONB * @property-read User $author * @property-read Table $table * @property-read Revision $revision @@ -46,17 +49,30 @@ class Proposal extends BaseModel return $this->belongsTo(Table::class); } - public function getChangesAttribute($value) + public function scopeUnmerged(Builder $query, Table $table) { - $changeset = Changeset::fromObject(fromJSON($value)); + return $query->whereRaw(' + "id" NOT IN + (SELECT "proposal_id" FROM "revisions" + LEFT JOIN "table_revision_pivot" + ON "revisions"."id" = "table_revision_pivot"."revision_id" + WHERE "table_revision_pivot"."table_id" = ?)', + [ + $table->getKey() + ]); + } + + public function getChangesetAttribute() + { + $changeset = Changeset::fromObject(fromJSON($this->attributes['changes'], true)); $changeset->revision = $this->revision; - $changeset->table = $this->table; + $changeset->table = $this->getAttribute('table'); $changeset->note = $this->note; return $changeset; } - public function setChangesAttribute($value) + public function setChangesetAttribute($value) { if ($value instanceof Changeset) { $this->attributes['changes'] = toJSON($value->toObject()); @@ -98,10 +114,96 @@ class Proposal extends BaseModel // relations 'table_id' => $changeset->table->getKey(), 'revision_id' => $changeset->revision->getKey(), - 'author_id' => \Auth::user()->getKey(), + 'author_id' => \user()->getKey(), // the proposal info 'note' => $changeset->note, - 'changes' => $changeset->toObject(), + 'changeset' => $changeset, // this is without a note ]); } + + /** + * Accept the proposed changes: create a new revision based on the parent revision + * and changes described in this Proposal, and link it to the table. + */ + public function createRevision() + { + DB::transaction(function () { + $changeset = $this->changeset; + + $columns = $changeset->fetchAndTransformColumns(); + + $newRevision = new Revision([ + 'ancestor_id' => $changeset->revision->getKey(), + 'proposal_id' => $this->getKey(), + 'note' => $changeset->note, + 'row_count' => 0, // Will be set later when we are sure about the row count + 'columns' => array_map(function(Column $c) { + return $c->toArray(false); + }, $columns), + ]); + + $newRevision->save(); // this gives it an ID, needed to associate rows + + // --- Copy over rows that are left unchanged --- + // ...this directly works with the pivot + $removedRowIds = (array)($changeset->removedRows ?? []); + $changedRowIds = array_keys((array)($changeset->rowUpdates ?? [])); + $excludedGRIDs = array_merge($removedRowIds, $changedRowIds); + + $excluded_ids = []; + + if ($excludedGRIDs) { + $questionmarks = str_repeat('?,', count($excludedGRIDs) - 1) . '?'; + $excluded_ids = $changeset->revision->rows()->whereRaw("data->'_id' IN ($questionmarks)", $excludedGRIDs) + ->get(['id'])->pluck('id')->toArray(); + } + + $query = ' + INSERT INTO revision_row_pivot + SELECT ? as revision_id, row_id FROM revision_row_pivot + WHERE revision_id = ?'; + $subs = [ + $newRevision->getKey(), + $changeset->revision->getKey() + ]; + if ($excluded_ids) { + $questionmarks = str_repeat('?,', count($excluded_ids) - 1) . '?'; + $query .= ' AND row_id NOT IN (' . $questionmarks . ')'; + $subs = array_merge($subs, $excluded_ids); + } + + DB::statement($query, $subs); + + // --- Insert modified rows --- + if ($changeset->rowUpdates) { + $ids = array_keys($changeset->rowUpdates); + $questionmarks = str_repeat('?,', count($ids) - 1) . '?'; + + $toChange = $changeset->revision->rows()->whereRaw("data->'_id' IN ($questionmarks)", $ids)->get(); + + $updateData = []; + foreach ($toChange as $row) { + $updateData[] = new Row([ + 'data' => $changeset->transformRow($row->data, false), + ]); + } + $newRevision->rows()->saveMany($updateData); + } + + // --- Insert new rows --- + if ($changeset->newRows) { + $newRowData = []; + foreach ($changeset->newRows as $newRow) { + $newRowData[] = new Row(['data' => $newRow]); + } + $newRevision->rows()->saveMany($newRowData); + } + + $newRevision->update(['row_count' => $newRevision->rows()->count()]); + + // --- Attach this revision to the table --- + $changeset->table->revisions()->save($newRevision); + $changeset->table->update(['revision_id' => $newRevision->getKey()]); + }); + } } diff --git a/app/Models/Revision.php b/app/Models/Revision.php index 550c31d..d0bf952 100644 --- a/app/Models/Revision.php +++ b/app/Models/Revision.php @@ -13,12 +13,13 @@ use Riesjart\Relaquent\Model\Concerns\HasRelaquentRelationships; * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @property int $ancestor_id + * @property int $proposal_id * @property string $note * @property object $columns * @property int $row_count - cached number of rows in the revision * @property-read Revision|null $parentRevision * @property-read Row[]|Collection $rows - * @property-read Proposal|null $sourceProposal - proposal that was used to create this revision + * @property-read Proposal|null $proposal - proposal that was used to create this revision * @property-read Proposal[]|Collection $dependentProposals */ class Revision extends BaseModel @@ -52,15 +53,15 @@ class Revision extends BaseModel } /** Proposal that lead to this revision */ - public function sourceProposal() + public function proposal() { - return $this->hasOneThrough(Proposal::class, 'revision_proposal_pivot'); + return $this->belongsTo(Proposal::class, 'proposal_id'); } /** Proposals that depend on this revision */ public function dependentProposals() { - return $this->hasMany(Proposal::class); + return $this->hasMany(Proposal::class, 'revision_id'); } /** Revision this orignates from */ diff --git a/app/Tables/Changeset.php b/app/Tables/Changeset.php index a5d9225..966f006 100644 --- a/app/Tables/Changeset.php +++ b/app/Tables/Changeset.php @@ -136,16 +136,17 @@ class Changeset * Note that the fromProposal() method should be used when the * proposal is available, as it populates additional fields. * - * @param \stdClass $changes + * @param \stdClass $object * @return Changeset */ - public static function fromObject($changes) + public static function fromObject($object) { + $object = (array)$object; $changeset = new Changeset(); foreach ($changeset->walkProps() as $prop) { - if (isset($changes->$prop)) { - $changeset->$prop = $changes->$prop; + if (isset($object[$prop])) { + $changeset->$prop = $object[$prop]; } } @@ -212,7 +213,7 @@ class Changeset } // if marked for removal, hide changes - if (!$row->_remove) { + if (!isset($row->_remove) || !$row->_remove) { // Changed values if (isset($this->rowUpdates[$row->_id])) { $newVals = $this->rowUpdates[$row->_id]; @@ -233,6 +234,7 @@ class Changeset } } + // move junk left over from the select unset($row->_row_pivot); if ($decorate) { @@ -857,6 +859,8 @@ class Changeset */ public function renumberRows() { + if (count($this->newRows) == 0) return; + $rows = []; $numerator = new RowNumerator(count($this->newRows)); foreach ($this->newRows as $row) { diff --git a/app/Tables/Column.php b/app/Tables/Column.php index d2fde28..4e792a7 100644 --- a/app/Tables/Column.php +++ b/app/Tables/Column.php @@ -154,18 +154,27 @@ class Column implements JsonSerializable, Arrayable /** * @return array with keys {name, title, type} */ - public function toArray() + public function toArray($decorate=true) { - return [ - 'id' => $this->id, - 'name' => $this->name, - 'title' => $this->title, - 'type' => $this->type, - '_new' => $this->isNew, - '_remove' => $this->toRemove, - '_changed' => $this->modified_attribs, - '_orig' => $this->orig_attribs, - ]; + if ($decorate) { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'title' => $this->title, + 'type' => $this->type, + '_new' => $this->isNew, + '_remove' => $this->toRemove, + '_changed' => $this->modified_attribs, + '_orig' => $this->orig_attribs, + ]; + } else { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'title' => $this->title, + 'type' => $this->type, + ]; + } } /** diff --git a/app/helpers.php b/app/helpers.php index 7842d80..bd3796a 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -9,7 +9,7 @@ const VALI_LINE = 'string|max:255'; const FEATURE_FORKS = false; const FEATURE_FAVES = false; const FEATURE_TABLE_COMMENTS = false; -const FEATURE_PROPOSALS = true; +const FEATURE_PROPOSALS = false; // global helpers function authed() { diff --git a/database/migrations/2018_07_08_193600_create_revisions_table.php b/database/migrations/2018_07_08_193600_create_revisions_table.php index 3a339b5..7dd37df 100644 --- a/database/migrations/2018_07_08_193600_create_revisions_table.php +++ b/database/migrations/2018_07_08_193600_create_revisions_table.php @@ -17,6 +17,7 @@ class CreateRevisionsTable extends Migration $table->increments('id'); $table->timestamps(); $table->unsignedInteger('ancestor_id')->index()->nullable(); // parent revision + $table->unsignedInteger('proposal_id')->index()->nullable(); // parent revision $table->unsignedInteger('row_count'); // cached nbr of rows diff --git a/database/migrations/2018_07_08_194000_create_proposals_table.php b/database/migrations/2018_07_08_194000_create_proposals_table.php index 73e89d2..b35714a 100644 --- a/database/migrations/2018_07_08_194000_create_proposals_table.php +++ b/database/migrations/2018_07_08_194000_create_proposals_table.php @@ -38,6 +38,12 @@ class CreateProposalsTable extends Migration $table->foreign('author_id')->references('id')->on('users') ->onDelete('cascade'); }); + + // add FK to revisions to point to the source proposal + Schema::table('revisions', function (Blueprint $table) { + $table->foreign('proposal_id')->references('id')->on('proposals') + ->onDelete('set null'); + }); } /** diff --git a/database/migrations/2018_07_08_194105_create_revision_proposal_pivot_table.php b/database/migrations/2018_07_08_194105_create_revision_proposal_pivot_table.php deleted file mode 100644 index bb31e86..0000000 --- a/database/migrations/2018_07_08_194105_create_revision_proposal_pivot_table.php +++ /dev/null @@ -1,37 +0,0 @@ -unsignedInteger('proposal_id')->index(); - $table->unsignedInteger('revision_id')->index(); - - $table->foreign('proposal_id')->references('id')->on('proposals') - ->onDelete('cascade'); - - $table->foreign('revision_id')->references('id')->on('revisions') - ->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('revision_proposal_pivot'); - } -} diff --git a/resources/assets/js/components/ColumnEditor.vue b/resources/assets/js/components/ColumnEditor.vue index e95653b..d1f139c 100644 --- a/resources/assets/js/components/ColumnEditor.vue +++ b/resources/assets/js/components/ColumnEditor.vue @@ -4,7 +4,7 @@ Complex animated column editor for the table edit page