Skip to content

Commit

Permalink
Merge pull request #14975 from craftcms/feature/cms-1302-show-provisi…
Browse files Browse the repository at this point in the history
…onal-draft-data-in-element-chips-cards

Show provisional draft data in element chips/cards/tables
  • Loading branch information
brandonkelly authored May 29, 2024
2 parents 896b56e + fe75b0b commit ca3264d
Show file tree
Hide file tree
Showing 19 changed files with 167 additions and 20 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Double-clicking anywhere within a table row on an element index page will now open the element’s editor slideout. ([#14379](https://github.com/craftcms/cms/discussions/14379))
- Element index checkboxes no longer have a lag when deselected, except within element selection modals. ([#14896](https://github.com/craftcms/cms/issues/14896))
- Relational field condition rules no longer factor in the target elements’ statuses or sites. ([#14989](https://github.com/craftcms/cms/issues/14989))
- Element cards now display provisional changes, with an “Edited” label. ([#14975](https://github.com/craftcms/cms/pull/14975))
- Improved mobile styling. ([#14910](https://github.com/craftcms/cms/pull/14910))
- Improved the look of slideouts.
- Table views within element index pages are no longer scrolled directly. ([#14927](https://github.com/craftcms/cms/pull/14927))
Expand Down Expand Up @@ -36,6 +37,7 @@
- Added the `{% expires %}` tag, which simplifies setting cache headers on the response. ([#14969](https://github.com/craftcms/cms/pull/14969))
- Added the `withCustomFields` element query param. ([#15003](https://github.com/craftcms/cms/pull/15003))
- Entry queries now support passing `*` to the `section` param, to filter the results to all section entries. ([#14978](https://github.com/craftcms/cms/discussions/14978))
- Element queries now support passing an element instance, or an array of element instances/IDs, to the `draftOf` param.
- Added `craft\elements\ElementCollection::find()`, which can return an element or elements in the collection based on a given element or ID. ([#15023](https://github.com/craftcms/cms/discussions/15023))
- Added `craft\elements\ElementCollection::fresh()`, which reloads each of the collection elements from the database. ([#15023](https://github.com/craftcms/cms/discussions/15023))
- `craft\elements\ElementCollection::contains()` now returns `true` if an element is passed in and the collection contains an element with the same ID and site ID; or if an integer is passed in and the collection contains an element with the same ID. ([#15023](https://github.com/craftcms/cms/discussions/15023))
Expand All @@ -62,6 +64,7 @@
- Added `craft\helpers\Cp::statusLabelHtml()`.
- Added `craft\helpers\DateTimeHelper::relativeTimeStatement()`.
- Added `craft\helpers\DateTimeHelper::relativeTimeToSeconds()`.
- Added `craft\helpers\ElementHelper::swapInProvisionalDrafts()`.
- Added `craft\helpers\StringHelper::indent()`.
- Added `craft\queue\Queue::getJobId()`.
- `craft\base\Element::defineTableAttributes()` now returns common attribute definitions used by most element types.
Expand Down
3 changes: 3 additions & 0 deletions src/base/Element.php
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,9 @@ public static function indexHtml(
]);
}

// See if there are any provisional drafts we should swap these out with
ElementHelper::swapInProvisionalDrafts($elements);

$variables['elements'] = $elements;
$template = '_elements/' . $viewState['mode'] . 'view/' . ($includeContainer ? 'container' : 'elements');

Expand Down
8 changes: 6 additions & 2 deletions src/controllers/AppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use craft\helpers\ArrayHelper;
use craft\helpers\Cp;
use craft\helpers\DateTimeHelper;
use craft\helpers\ElementHelper;
use craft\helpers\Html;
use craft\helpers\Json;
use craft\helpers\Search;
Expand Down Expand Up @@ -762,17 +763,20 @@ public function actionRenderElements(): Response
->id($id)
->fixedOrder()
->drafts(null)
->provisionalDrafts(null)
->revisions(null)
->siteId($siteId)
->status(null)
->all();

// See if there are any provisional drafts we should swap these out with
ElementHelper::swapInProvisionalDrafts($elements);

foreach ($elements as $element) {
foreach ($instances as $key => $instance) {
$id = $element->isProvisionalDraft ? $element->getCanonicalId() : $element->id;
/** @var 'chip'|'card' $ui */
$ui = $instance['ui'] ?? 'chip';
$elementHtml[$element->id][$key] = match ($ui) {
$elementHtml[$id][$key] = match ($ui) {
'chip' => Cp::elementChipHtml($element, $instance),
'card' => Cp::elementCardHtml($element, $instance),
};
Expand Down
19 changes: 15 additions & 4 deletions src/controllers/ElementIndexesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1041,16 +1041,27 @@ public function actionElementTableHtml(): Response
throw new ForbiddenHttpException('User not authorized to edit content for this site.');
}

// check for a provisional draft first
/** @var ElementInterface|null $element */
$element = $elementType::find()
->id($id)
->drafts(null)
->provisionalDrafts(null)
->revisions(null)
->draftOf($id)
->provisionalDrafts()
->siteId($siteId)
->status(null)
->one();

if (!$element) {
/** @var ElementInterface|null $element */
$element = $elementType::find()
->id($id)
->drafts(null)
->provisionalDrafts(null)
->revisions(null)
->siteId($siteId)
->status(null)
->one();
}

if (!$element) {
throw new BadRequestHttpException("Invalid element ID: $id");
}
Expand Down
3 changes: 3 additions & 0 deletions src/elements/NestedElementManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ function(string $id, array $config, $attribute, &$settings) use ($owner) {
->all();
}

// See if there are any provisional drafts we should swap these out with
ElementHelper::swapInProvisionalDrafts($elements);

$this->setOwnerOnNestedElements($owner, $elements);

if (!empty($elements)) {
Expand Down
22 changes: 20 additions & 2 deletions src/elements/db/ElementQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ class ElementQuery extends Query implements ElementQueryInterface
* This can be set to one of the following:
*
* - A source element ID – matches drafts of that element
* - A source element
* - An array of source elements or element IDs
* - `'*'` – matches drafts of any source element
* - `false` – matches unpublished drafts that have no source element
*
Expand Down Expand Up @@ -701,11 +703,27 @@ public function draftId(?int $value = null): static
*/
public function draftOf($value): static
{
$valid = false;
if ($value instanceof ElementInterface) {
$this->draftOf = $value->getCanonicalId();
} elseif (is_numeric($value) || $value === '*' || $value === false || $value === null) {
$valid = true;
} elseif (
is_numeric($value) ||
(is_array($value) && ArrayHelper::isNumeric($value)) ||
$value === '*' ||
$value === false ||
$value === null
) {
$this->draftOf = $value;
} else {
$valid = true;
} elseif (is_array($value) && !empty($value)) {
$c = Collection::make($value);
if ($c->every(fn($v) => $v instanceof ElementInterface || is_numeric($v))) {
$this->draftOf = $c->map(fn($v) => $v instanceof ElementInterface ? $v->id : $v)->all();
$valid = true;
}
}
if (!$valid) {
throw new InvalidArgumentException('Invalid draftOf value');
}
if ($value !== null && $this->drafts === false) {
Expand Down
2 changes: 2 additions & 0 deletions src/elements/db/ElementQueryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ public function draftId(?int $value = null): static;
* | Value | Fetches drafts…
* | - | -
* | `1` | for the {element} with an ID of 1.
* | `[1, 2]` | for the {elements} with an ID of 1 or 2.
* | a [[{element-class}]] object | for the {element} represented by the object.
* | an array of [[{element-class}]] objects | for the {elements} represented by the objects.
* | `'*'` | for any {element}
* | `false` | that aren’t associated with a published {element}
*
Expand Down
1 change: 1 addition & 0 deletions src/fields/BaseRelationField.php
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,7 @@ protected function inputTemplateVariables(array|ElementQueryInterface $value = n
{
if ($value instanceof ElementQueryInterface) {
$value = $value->all();
ElementHelper::swapInProvisionalDrafts($value);
} elseif (!is_array($value)) {
$value = [];
}
Expand Down
28 changes: 25 additions & 3 deletions src/helpers/Cp.php
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,10 @@ public static function elementChipHtml(ElementInterface $element, array $config
$config['attributes'],
fn() => $element->getChipLabelHtml(),
);

if ($element->isProvisionalDraft) {
$config['labelHtml'] .= self::changeStatusLabelHtml();
}
}

$html = static::chipHtml($element, $config);
Expand Down Expand Up @@ -560,9 +564,16 @@ public static function elementCardHtml(ElementInterface $element, array $config
$headingContent = self::elementLabelHtml($element, $config, $attributes, fn() => Html::encode($element->getUiLabel()));
$bodyContent = $element->getCardBodyHtml() ?? '';

$statusLabel = $element::hasStatuses() ? static::componentStatusLabelHtml($element) : null;
if ($statusLabel) {
$bodyContent .= Html::tag('div', $statusLabel, ['class' => 'flex']);
$labels = array_filter([
$element::hasStatuses() ? static::componentStatusLabelHtml($element) : null,
$element->isProvisionalDraft ? self::changeStatusLabelHtml() : null,
]);

if (!empty($labels)) {
$bodyContent .= Html::ul($labels, [
'class' => ['flex', 'gap-xs'],
'encode' => false,
]);
}

$thumb = $element->getThumbHtml(128);
Expand Down Expand Up @@ -743,6 +754,15 @@ public static function statusLabelHtml(array $config = []): ?string
]);
}

private static function changeStatusLabelHtml(): string
{
return static::statusLabelHtml([
'color' => Color::Blue,
'icon' => 'pen-circle',
'label' => Craft::t('app', 'Edited'),
]);
}

/**
* Renders status label HTML for a [[Statusable]] component.
*
Expand Down Expand Up @@ -793,9 +813,11 @@ private static function baseElementAttributes(ElementInterface $element, array $
'data' => array_filter([
'type' => get_class($element),
'id' => $element->id,
'canonical-id' => $element->getCanonicalId(),
'draft-id' => $element->draftId,
'revision-id' => $element->revisionId,
'site-id' => $element->siteId,
'provisional' => $element->isProvisionalDraft,
'status' => $element->getStatus(),
'label' => (string)$element,
'url' => $element->getUrl(),
Expand Down
40 changes: 40 additions & 0 deletions src/helpers/ElementHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -916,4 +916,44 @@ public static function renderElements(array $elements, array $variables = []): M

return new Markup(implode("\n", $output), Craft::$app->charset);
}

/**
* Swaps out any canonical elements with provisional drafts, when they exist.
*
* @param ElementInterface[] $elements
* @since 5.2.0
*/
public static function swapInProvisionalDrafts(array &$elements): void
{
$canonicalElements = array_filter($elements, fn(ElementInterface $element) => $element->getIsCanonical());

if (empty($canonicalElements)) {
return;
}

$first = reset($canonicalElements);

if (!$first::hasDrafts()) {
return;
}

$drafts = $first::find()
->draftOf($canonicalElements)
->provisionalDrafts()
->siteId($first->siteId)
->status(null)
->indexBy('canonicalId')
->all();

if (empty($drafts)) {
return;
}

// array_filter() preserves keys, so it's safe to loop through it rather than $elements here
foreach ($canonicalElements as $i => $element) {
if (isset($drafts[$element->id])) {
$elements[$i] = $drafts[$element->id];
}
}
}
}
1 change: 1 addition & 0 deletions src/icons/solid/pen-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions src/templates/_elements/tableview/elements.twig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@
: 0 %}
{% set elementTitle = element.title ?: element.id %}
{% set showInputs = (inlineEditing ?? false) and elementsService.canSave(element, currentUser) %}
<tr data-id="{{ element.id }}"{% if structure %} data-title="{{ elementTitle }}" data-level="{{ element.level }}" data-descendants="{{ totalDescendants ?? 0 }}"{% endif %}{% if element.id in disabledElementIds %} class="disabled"{% endif %}>
{% tag 'tr' with {
data: {
id: element.id,
'canonical-id': element.getCanonicalId(),
title: elementTitle,
level: structure ? element.level : false,
descendants: structure ? (totalDescendants ?? 0) : false,
},
class: {
disabled: element.id in disabledElementIds,
}|filter|keys,
} %}
{% if selectable %}
<td class="checkbox-cell">
{{ tag('div', {
Expand Down Expand Up @@ -95,7 +106,7 @@
</td>
{% endif %}
{% endfor %}
</tr>
{% endtag %}
{% endfor %}

{% endapply -%}
1 change: 1 addition & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,7 @@
'This year' => 'This year',
'This {type} doesn’t have revisions.' => 'This {type} doesn’t have revisions.',
'This {type} has been updated.' => 'This {type} has been updated.',
'This {type} has unsaved changes.' => 'This {type} has unsaved changes.',
'Time Zone' => 'Time Zone',
'Time fields are better suited for managing Time-only values.' => 'Time fields are better suited for managing Time-only values.',
'Time to reserve' => 'Time to reserve',
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/cp/dist/cp.js.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/web/assets/cp/src/js/Craft.js
Original file line number Diff line number Diff line change
Expand Up @@ -2371,7 +2371,9 @@ $.extend(Craft, {
},

refreshElementInstances(elementId) {
const $elements = $(`div.element[data-id="${elementId}"][data-settings]`);
const $elements = $(
`div.element[data-id="${elementId}"][data-settings],div.element[data-canonical-id="${elementId}"][data-provisional][data-settings]`
);
if (!$elements.length) {
return;
}
Expand Down
17 changes: 17 additions & 0 deletions src/web/assets/cp/src/js/ElementEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ Craft.ElementEditor = Garnish.Base.extend(
})
.then((response) => {
Craft.cp.displaySuccess(response.data.message);

// Broadcast a saveMessage event, in case any chips/cards should be
// updated to stop showing the provisional changes
Craft.broadcaster.postMessage({
event: 'saveElement',
id: this.settings.canonicalId,
});

this.slideout.close();
})
.catch(reject);
Expand Down Expand Up @@ -1826,6 +1834,15 @@ Craft.ElementEditor = Garnish.Base.extend(
}

this.trigger('update');

if (this.settings.isProvisionalDraft && Craft.broadcaster) {
// Broadcast a saveMessage event, in case any chips/cards should be
// updated to show the provisional changes
Craft.broadcaster.postMessage({
event: 'saveElement',
id: this.settings.canonicalId,
});
}
},

setStatusMessage: function (message) {
Expand Down
14 changes: 11 additions & 3 deletions src/web/assets/cp/src/js/ElementEditorSlideout.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,21 @@ Craft.ElementEditorSlideout = Craft.CpScreenSlideout.extend(

if (this.settings.elementId) {
params.elementId = this.settings.elementId;
} else if (this.$element && this.$element.data('id')) {
params.elementId = this.$element.data('id');
} else if (this.$element) {
if (this.$element.data('canonical-id')) {
params.elementId = this.$element.data('canonical-id');
} else if (this.$element.data('id')) {
params.elementId = this.$element.data('id');
}
}

if (this.settings.draftId) {
params.draftId = this.settings.draftId;
} else if (this.$element && this.$element.data('draft-id')) {
} else if (
this.$element &&
this.$element.data('draft-id') &&
!Garnish.hasAttr(this.$element, 'data-provisional')
) {
params.draftId = this.$element.data('draft-id');
} else if (this.settings.revisionId) {
params.revisionId = this.settings.revisionId;
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/cp/src/js/TableElementIndexView.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Craft.TableElementIndexView = Craft.BaseElementIndexView.extend({
this._broadcastListener = (ev) => {
if (ev.data.event === 'saveElement') {
const $rows = this.$table.find(
`> tbody > tr[data-id="${ev.data.id}"]`
`> tbody > tr[data-id="${ev.data.id}"],> tbody > tr[data-canonical-id="${ev.data.id}"]`
);
if ($rows.length) {
const data = {
Expand Down

0 comments on commit ca3264d

Please sign in to comment.