Skip to main content

Timeline

With this module you can add a timeline to your resource, usually for its detail page.

Installation​

composer require qore/timeline

After you installed the module publish its contents:

php artisan vendor:publish --tag=qore.timeline.db --force
php artisan vendor:publish --tag=qore.timeline.frontend --force
# Optional:
php artisan vendor:publish --tag=qore.timeline.config

After you have published the contents of the module run the migrations:

php artisan tenants:migrate

After you have installed the module you need to enable it in your Qore application.

Usage​

This module needs some setup from your side, as it is built to be flexible. In the example below, we will add a timeline to the SalesDealResource.

Step 1: Implement interface​

Add the following interface and trait to your resource:

class SalesDealResource extends QoreResource implements ResourceTimelineContract
{
use HasResourceTimeline;
}

Then define the items that should be shown in the timeline:

/**
* @return array<string, AbstractResourceTimelineItem>
*/
public function getResourceTimelineItemDefinitions(): array
{
return [
ResourceTimelineActionType::NOTE->value => new ResourceTimelineNoteItem,
TimelineActionType::MARK_DEAL_AS_WON->value => new DealWonTimelineItem,
TimelineActionType::MARK_DEAL_AS_LOST->value => new DealLostTimelineItem,
];
}

An item should extend the AbstractResourceTimelineItem class, e.g.:

class DealWonTimelineItem extends AbstractResourceTimelineItem
{
public function component(): string
{
return 'DealWonTimelineItem';
}

public function jsonResourceClass(): string
{
return DealWonItemJsonResource::class;
}

public function iconColor(): string
{
return 'positive';
}
}

As you can see, we also need to define a JSON resource for the item. This will be used to transform the data for the front-end component.

/**
* @mixin ResourceTimelineItem
*/
class DealWonItemJsonResource extends JsonResource
{
public function toArray(Request $request): array
{
$wonAt = Carbon::parse($this->payload['won_at']);

return [
'won_at' => date_time_presenter($wonAt, asDiffForHumans: true),
];
}
}

Step 2: Add actions​

Now you can add timeline actions to your resource that will create timeline items. These are normal QoreResource actions but tailored to fit more into the timeline.

public function getResourceTimelineActions(ResourceTimeline $timeline): ResourceTimelineActionCollection
{
return ResourceTimelineActionCollection::make([
new NoteResourceTimelineAction($timeline),
new MarkDealAsWonAction($timeline),
new MarkDealAsLostAction($timeline),
]);
}

To define the action:

class MarkDealAsWonAction extends ResourceTimelineAction
{
public function __construct(ResourceTimeline $timeline)
{
parent::__construct($timeline);

$this->canRun(fn (SalesDeal $deal) => $deal->status !== SalesDealStatus::WON);
}

public function title(): string
{
return ___('sales::sales.won');
}

public function icon(): string
{
return MaterialIcons::CHECKCIRCLE->value;
}

public function typeName(): string
{
return TimelineActionType::MARK_DEAL_AS_WON->value;
}

/**
* @param SalesDeal $model
* @param array<string, mixed> $arguments
*/
public function run(Model $model, array $arguments): void
{
$wonAt = now()->setTimezone(config('app.timezone'));

$model->won_at = $wonAt;
$model->status = SalesDealStatus::WON;
$model->save();

$item = new_resource_timeline_item(
timeline: $this->timeline,
typeName: $this->typeName(),
title: ___('sales::sales.won_deal'),
icon: $this->icon(),
payload: [
'won_at' => $wonAt->format('Y-m-d H:i:s'),
]
);

$item->save();
}
}

As you can see, the action also has a typeName. This is used to link the action to the timeline item definition. In some cases, like a Note, you might want the user to be able to edit the contents of your item:

class NoteResourceTimelineAction extends ResourceTimelineAction
{
// ...

public function editable(): bool
{
return true;
}

public function editItemFields(ResourceTimelineItem $item): FieldCollection
{
return $this->myFields($item);
}

/**
* @param array<string, mixed> $arguments
*/
public function updateItem(ResourceTimelineItem $item, array $arguments): void
{
$item->title = $arguments['title'] ?? $this->title();
$item->payload = [
'note' => $arguments['note']
];
$item->save();
}
}

Step 3: Resource Front-end​

First, override the component for the detail page:

public function resourceDetailComponent(): string
{
return 'SalesDealDetail';
}

Create the vue component:

<template>
<ResourceTimeline
resource-name="sales_deals"
:model-id="model.id"
v-on="$listeners"
/>
</template>

<script>
export default {
props: {
model: {
type: Object,
required: true
}
}
}
</script>

And register it somewhere globally in your project:

Vue.component('SalesDealDetail', () => import('./Details/SalesDealDetail'))

Step 4: Items front-end​

Finally, you need to create the front-end components for your timeline items.

Create a new Vue component:

<template>
<ResourceTimelineItemWrapper
:item="item"
:expandable="false"
:editable="false"
:deletable="false"
:pinnable="false"
v-on="$listeners"
>
<div class="q-pa-md bg-green-1">
<div class="flex gap-sm items-center">
<q-icon
name="check_circle"
color="positive"
size="md"
/>

{{ item.won_at }}
</div>
</div>
</ResourceTimelineItemWrapper>
</template>

<script>
export default {
props: {
item: {
type: Object,
required: true
},
resourceName: {
type: String,
required: true
}
}
}
</script>

You don't need to use the ResourceTimelineItemWrapper but it gives you some default styles and other features like expand/edit/delete/pin buttons.

Then register it globally:

Vue.component('DealWonTimelineItem', () => import('./TimelineItems/DealWonTimelineItem'))

Configurations​

Overriding defaults​

You can override the following methods to your resource if necessary:

public function getResourceTimeline(Model $model, string $name = 'default'): ResourceTimeline;

public function canViewResourceTimeline(ResourceTimeline $timeline): bool;

public function canAddItemsToResourceTimeline(ResourceTimeline $timeline): bool;

public function canEditItemsInResourceTimeline(ResourceTimelineItem $item): bool;

public function canRemoveItemsFromResourceTimeline(ResourceTimelineItem $item): bool;

public function getResourceTimelineActions(ResourceTimeline $timeline): ResourceTimelineActionCollection;

public function getTimelineTitle(ResourceTimeline $timeline): string;

/**
* @return array<string, AbstractResourceTimelineItem>
*/
public function getResourceTimelineItemDefinitions(): array;

Expanding items​

You can conditionally show more details for an item by making it expandable, e.g. for a Note item:

<template>
<ResourceTimelineItemWrapper
ref="wrapper"
:item="item"
:expanded="expanded"
v-on="$listeners"
@expanded="(val) => expanded = val"
>
<div
class="bg-yellow-2 q-pa-md white"
style="white-space: pre-line;"
>
<span v-if="!expanded">{{ truncatedNote }}</span>

<CKEditorDetail
v-if="expanded"
:key="item.note"
:field="item.note_field"
:value="item.note"
/>
</div>
</ResourceTimelineItemWrapper>
</template>

<script>
export default {
...
data () {
return {
expanded: false
}
},
computed: {
truncatedNote () {
return this.$truncate(this.item.note_string, 200)
}
},
mounted () {
this.expanded = true
}
}
</script>