Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions resources/js/bootstrap/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import AssetContainerList from '../components/AssetContainerList.vue';
import AddonList from '../components/AddonList.vue';
import AddonDetails from '../components/AddonDetails.vue';
import CollectionWidget from '../components/entries/CollectionWidget.vue';
import FormWidget from '../components/forms/FormWidget.vue';
import SvgIcon from '../components/SvgIcon.vue';
import FileIcon from '../components/FileIcon.vue';
import LoadingGraphic from '../components/LoadingGraphic.vue';
Expand Down Expand Up @@ -115,6 +116,7 @@ export default function registerGlobalComponents(app) {

// Widgets
app.component('collection-widget', CollectionWidget);
app.component('form-widget', FormWidget);

// Reusable
app.component('svg-icon', SvgIcon);
Expand Down
54 changes: 54 additions & 0 deletions resources/js/components/DateFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,66 @@ export default class DateFormatter {

toString() {
try {
if (this.#options.relative) return this.#formatRelative();

return Intl.DateTimeFormat(this.locale, this.#options).format(this.#date);
} catch (e) {
return 'Invalid Date';
}
}

#formatRelative() {
let specificity = this.#options.relative;
specificity = !specificity || specificity === true ? 'year' : specificity;

const now = new Date();
const diff = now - this.#date;
const seconds = Math.abs(Math.floor(diff / 1000));
const minutes = Math.abs(Math.floor(seconds / 60));
const hours = Math.abs(Math.floor(minutes / 60));
const days = Math.abs(Math.floor(hours / 24));
const weeks = Math.abs(Math.floor(days / 7));
const months = Math.abs(Math.floor(days / 30));
const years = Math.abs(Math.floor(days / 365));

const rtf = new Intl.RelativeTimeFormat(this.locale, { numeric: 'auto' });

// Always show seconds if less than a minute
if (seconds < 60) return rtf.format(-Math.sign(diff) * seconds, 'second');

// Show minutes if less than an hour and specificity allows
if (minutes < 60 && ['minute', 'hour', 'day', 'week', 'month', 'year'].includes(specificity)) {
return rtf.format(-Math.sign(diff) * minutes, 'minute');
}

// Show hours if less than a day and specificity allows
if (hours < 24 && ['hour', 'day', 'week', 'month', 'year'].includes(specificity)) {
return rtf.format(-Math.sign(diff) * hours, 'hour');
}

// Show days if less than a week and specificity allows
if (days < 7 && ['day', 'week', 'month', 'year'].includes(specificity)) {
return rtf.format(-Math.sign(diff) * days, 'day');
}

// Show weeks if less than a month and specificity allows
if (weeks < 4 && ['week', 'month', 'year'].includes(specificity)) {
return rtf.format(-Math.sign(diff) * weeks, 'week');
}

// Show months if less than a year and specificity allows
if (months < 12 && ['month', 'year'].includes(specificity)) {
return rtf.format(-Math.sign(diff) * months, 'month');
}

// Show years if specificity allows
if (specificity === 'year') {
return rtf.format(-Math.sign(diff) * years, 'year');
}

return new DateFormatter(this.#date, this.#options.fallback || 'datetime').toString();
}

static format(date, options) {
return new DateFormatter(date, options).toString();
}
Expand Down
82 changes: 82 additions & 0 deletions resources/js/components/forms/FormWidget.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script>
import Listing from '../Listing.vue';
import DateFormatter from '@statamic/components/DateFormatter.js';
import { Widget } from '@statamic/ui';

export default {
mixins: [Listing],

components: {
Widget,
},

props: {
form: { type: String, required: true },
fields: { type: Array, default: () => [] },
title: { type: String },
},

data() {
return {
cols: [
...this.fields.map((field) => ({ label: field, field, visible: true })),
{ label: 'Date', field: 'date', visible: true },
],
listingKey: 'submissions',
requestUrl: cp_url(`forms/${this.form}/submissions`),
};
},

methods: {
formatDate(value) {
return DateFormatter.format(value, { relative: 'hour' }).toString();
},
},
};
</script>

<template>
<Widget :title="title" icon="forms">
<data-list v-if="!initializing && items.length" :rows="items" :columns="cols" :sort="false" class="w-full">
<div v-if="initializing" class="loading">
<loading-graphic />
</div>

<data-list-table
v-else
:loading="loading"
unstyled
class="[&_td]:px-0.5 [&_td]:py-0.75 [&_td]:text-sm [&_thead]:hidden"
>
<template v-for="field in fields" #[`cell-${field}`]="{ row: submission }">
<a
:href="cp_url(`forms/${form}/submissions/${submission.id}`)"
class="line-clamp-1 overflow-hidden text-ellipsis"
>
{{ submission[field] }}
</a>
</template>
<template #cell-date="{ row: submission }">
<div
class="text-end font-mono text-xs whitespace-nowrap text-gray-500 antialiased"
v-html="formatDate(submission.datestamp)"
/>
</template>
</data-list-table>
</data-list>

<p v-if="!initializing && !items.length" class="p-3 text-center text-sm text-gray-600">
{{ __('This form is awaiting responses') }}
</p>

<template #actions>
<data-list-pagination
v-if="meta.last_page != 1"
:resource-meta="meta"
@page-selected="selectPage"
:scroll-to-top="false"
:show-page-links="false"
/>
</template>
</Widget>
</template>
2 changes: 1 addition & 1 deletion resources/js/components/ui/Widget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defineProps({

<template>
<ui-card inset>
<div class="flex h-full flex-col justify-between">
<div class="flex h-full flex-col justify-between min-h-54">
<div>
<header class="flex items-center justify-between px-4 py-2.5 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
Expand Down
3 changes: 2 additions & 1 deletion resources/js/components/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import { default as DatePicker } from './Datepicker/Index.vue';
import { default as DateRangePicker } from './DateRangePicker/Index.vue';
import { default as TimePicker } from './TimePicker/Index.vue';
import { default as Button } from './Button/Index.vue';
import { default as Widget } from './Widget.vue';

export { WithField, Calendar, Card, DatePicker, DateRangePicker, TimePicker, Button };
export { WithField, Calendar, Card, DatePicker, DateRangePicker, TimePicker, Button, Widget };
103 changes: 103 additions & 0 deletions resources/js/tests/components/DateFormatter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,106 @@ test.each([
test('an invalid preset throws an error', () => {
expect(() => new DateFormatter().options('foo')).toThrow('Invalid date format: foo');
});

test.each([
// All the different diffs of time. It defaults to 'year' specificity.
['now', 'en', { relative: true }, '2021-12-25T12:13:14Z', 'now'],
['seconds ago', 'en', { relative: true }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['minute ago', 'en', { relative: true }, '2021-12-25T12:12:14Z', '1 minute ago'],
['minutes ago', 'en', { relative: true }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['hour ago', 'en', { relative: true }, '2021-12-25T11:13:14Z', '1 hour ago'],
['hours ago', 'en', { relative: true }, '2021-12-25T10:13:14Z', '2 hours ago'],
['day ago', 'en', { relative: true }, '2021-12-24T12:13:14Z', 'yesterday'],
['days ago', 'en', { relative: true }, '2021-12-23T12:13:14Z', '2 days ago'],
['week ago', 'en', { relative: true }, '2021-12-18T12:13:14Z', 'last week'],
['weeks ago', 'en', { relative: true }, '2021-12-11T12:13:14Z', '2 weeks ago'],
['month ago', 'en', { relative: true }, '2021-11-25T12:13:14Z', 'last month'],
['months ago', 'en', { relative: true }, '2021-10-25T12:13:14Z', '2 months ago'],
['year ago', 'en', { relative: true }, '2020-12-25T12:13:14Z', 'last year'],
['years ago', 'en', { relative: true }, '2019-12-25T12:13:14Z', '2 years ago'],

// Same in another locale
['de, now', 'de', { relative: true }, '2021-12-25T12:13:14Z', 'jetzt'],
['de, seconds ago', 'de', { relative: true }, '2021-12-25T12:13:12Z', 'vor 2 Sekunden'],
['de, minute ago', 'de', { relative: true }, '2021-12-25T12:12:14Z', 'vor 1 Minute'],
['de, minutes ago', 'de', { relative: true }, '2021-12-25T12:11:14Z', 'vor 2 Minuten'],
['de, hour ago', 'de', { relative: true }, '2021-12-25T11:13:14Z', 'vor 1 Stunde'],
['de, hours ago', 'de', { relative: true }, '2021-12-25T10:13:14Z', 'vor 2 Stunden'],
['de, day ago', 'de', { relative: true }, '2021-12-24T12:13:14Z', 'gestern'],
['de, days ago', 'de', { relative: true }, '2021-12-23T12:13:14Z', 'vorgestern'],
['de, week ago', 'de', { relative: true }, '2021-12-18T12:13:14Z', 'letzte Woche'],
['de, weeks ago', 'de', { relative: true }, '2021-12-11T12:13:14Z', 'vor 2 Wochen'],
['de, month ago', 'de', { relative: true }, '2021-11-25T12:13:14Z', 'letzten Monat'],
['de, months ago', 'de', { relative: true }, '2021-10-25T12:13:14Z', 'vor 2 Monaten'],
['de, year ago', 'de', { relative: true }, '2020-12-25T12:13:14Z', 'letztes Jahr'],
['de, years ago', 'de', { relative: true }, '2019-12-25T12:13:14Z', 'vor 2 Jahren'],

// All different specificities
['years ago, spec year', 'en', { relative: 'year' }, '2019-12-25T12:13:14Z', '2 years ago'],
['years ago, spec month', 'en', { relative: 'month' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['years ago, spec week', 'en', { relative: 'week' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['years ago, spec day', 'en', { relative: 'day' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['years ago, spec hour', 'en', { relative: 'hour' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['years ago, spec minute', 'en', { relative: 'minute' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['years ago, spec second', 'en', { relative: 'second' }, '2019-12-25T12:13:14Z', '12/25/2019, 12:13 PM'],
['months ago, spec year', 'en', { relative: 'year' }, '2021-10-25T12:13:14Z', '2 months ago'],
['months ago, spec month', 'en', { relative: 'month' }, '2021-10-25T12:13:14Z', '2 months ago'],
['months ago, spec week', 'en', { relative: 'week' }, '2021-10-25T12:13:14Z', '10/25/2021, 12:13 PM'],
['months ago, spec day', 'en', { relative: 'day' }, '2021-10-25T12:13:14Z', '10/25/2021, 12:13 PM'],
['months ago, spec hour', 'en', { relative: 'hour' }, '2021-10-25T12:13:14Z', '10/25/2021, 12:13 PM'],
['months ago, spec minute', 'en', { relative: 'minute' }, '2021-10-25T12:13:14Z', '10/25/2021, 12:13 PM'],
['months ago, spec second', 'en', { relative: 'second' }, '2021-10-25T12:13:14Z', '10/25/2021, 12:13 PM'],
['weeks ago, spec year', 'en', { relative: 'year' }, '2021-12-11T12:13:14Z', '2 weeks ago'],
['weeks ago, spec month', 'en', { relative: 'month' }, '2021-12-11T12:13:14Z', '2 weeks ago'],
['weeks ago, spec week', 'en', { relative: 'week' }, '2021-12-11T12:13:14Z', '2 weeks ago'],
['weeks ago, spec day', 'en', { relative: 'day' }, '2021-12-11T12:13:14Z', '12/11/2021, 12:13 PM'],
['weeks ago, spec hour', 'en', { relative: 'hour' }, '2021-12-11T12:13:14Z', '12/11/2021, 12:13 PM'],
['weeks ago, spec minute', 'en', { relative: 'minute' }, '2021-12-11T12:13:14Z', '12/11/2021, 12:13 PM'],
['weeks ago, spec second', 'en', { relative: 'second' }, '2021-12-11T12:13:14Z', '12/11/2021, 12:13 PM'],
['days ago, spec year', 'en', { relative: 'year' }, '2021-12-23T12:13:14Z', '2 days ago'],
['days ago, spec month', 'en', { relative: 'month' }, '2021-12-23T12:13:14Z', '2 days ago'],
['days ago, spec week', 'en', { relative: 'week' }, '2021-12-23T12:13:14Z', '2 days ago'],
['days ago, spec day', 'en', { relative: 'day' }, '2021-12-23T12:13:14Z', '2 days ago'],
['days ago, spec hour', 'en', { relative: 'hour' }, '2021-12-23T12:13:14Z', '12/23/2021, 12:13 PM'],
['days ago, spec minute', 'en', { relative: 'minute' }, '2021-12-23T12:13:14Z', '12/23/2021, 12:13 PM'],
['days ago, spec second', 'en', { relative: 'second' }, '2021-12-23T12:13:14Z', '12/23/2021, 12:13 PM'],
['hours ago, spec year', 'en', { relative: 'year' }, '2021-12-25T10:13:14Z', '2 hours ago'],
['hours ago, spec month', 'en', { relative: 'month' }, '2021-12-25T10:13:14Z', '2 hours ago'],
['hours ago, spec week', 'en', { relative: 'week' }, '2021-12-25T10:13:14Z', '2 hours ago'],
['hours ago, spec day', 'en', { relative: 'day' }, '2021-12-25T10:13:14Z', '2 hours ago'],
['hours ago, spec hour', 'en', { relative: 'hour' }, '2021-12-25T10:13:14Z', '2 hours ago'],
['hours ago, spec minute', 'en', { relative: 'minute' }, '2021-12-25T10:13:14Z', '12/25/2021, 10:13 AM'],
['hours ago, spec second', 'en', { relative: 'second' }, '2021-12-25T10:13:14Z', '12/25/2021, 10:13 AM'],
['minutes ago, spec year', 'en', { relative: 'year' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec month', 'en', { relative: 'month' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec week', 'en', { relative: 'week' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec day', 'en', { relative: 'day' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec hour', 'en', { relative: 'hour' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec minute', 'en', { relative: 'minute' }, '2021-12-25T12:11:14Z', '2 minutes ago'],
['minutes ago, spec second', 'en', { relative: 'second' }, '2021-12-25T12:11:14Z', '12/25/2021, 12:11 PM'],
['seconds ago, spec year', 'en', { relative: 'year' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec month', 'en', { relative: 'month' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec week', 'en', { relative: 'week' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec day', 'en', { relative: 'day' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec hour', 'en', { relative: 'hour' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec minute', 'en', { relative: 'minute' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
['seconds ago, spec second', 'en', { relative: 'second' }, '2021-12-25T12:13:12Z', '2 seconds ago'],
[
'fallback of datetime preset',
'en',
{ relative: 'second', fallback: 'datetime' },
'2021-12-20T12:13:14Z',
'12/20/2021, 12:13 PM',
],
['fallback of time preset', 'en', { relative: 'second', fallback: 'time' }, '2021-12-20T12:13:14Z', '12:13 PM'],
[
'fallback of options',
'en',
{ relative: 'second', fallback: { dateStyle: 'long', timeStyle: 'long' } },
'2021-12-20T12:13:14Z',
'December 20, 2021 at 12:13:14 PM UTC',
],
])('it can use relative format (%s)', (label, locale, options, date, expected) => {
setNavigatorLanguage(locale);
expect(new DateFormatter().format(date, options)).toBe(expected);
});
51 changes: 6 additions & 45 deletions resources/views/forms/widget.blade.php
Original file line number Diff line number Diff line change
@@ -1,45 +1,6 @@
@php
use function Statamic\trans as __;
@endphp

@php
use Statamic\Support\Arr;
@endphp

<div class="card overflow-hidden p-0">
<div class="flex items-center justify-between border-b p-4 dark:border-b dark:border-dark-900 dark:bg-dark-650">
<h2>
<a class="flex items-center" href="{{ $form->showUrl() }}">
<div class="h-6 w-6 text-gray-800 dark:text-dark-200 ltr:mr-2 rtl:ml-2">
@cp_svg('icons/light/drawer-file')
</div>
<span v-pre>{{ $title }}</span>
</a>
</h2>
</div>
<div>
@if (! $submissions)
<p class="p-4 text-sm text-gray-600">{{ __('This form is awaiting responses') }}</p>
@else
<table class="data-table">
@foreach ($submissions as $submission)
<tr>
@foreach ($fields as $key => $field)
<td>
<a
href="{{ cp_route('forms.submissions.show', [$form->handle(), $submission['id']]) }}"
>
{{ Arr::get($submission, $field) }}
</a>
</td>
@endforeach

<td class="ltr:text-right rtl:text-left">
{{ $submission['date']->diffInDays() <= 14 ? $submission['date']->diffForHumans() : $submission['date']->asVueComponent() }}
</td>
</tr>
@endforeach
</table>
@endif
</div>
</div>
<form-widget
form="{{ $form->handle() }}"
title="{{ $title }}"
:fields='@json($fields)'
:initial-per-page="{{ $limit }}"
></form-widget>
1 change: 1 addition & 0 deletions src/Forms/Widget.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function html()
'fields' => $this->config('fields', []),
'submissions' => collect($form->submissions())->reverse()->take((int) $this->config('limit', 5))->toArray(),
'title' => $this->config('title', $form->title()),
'limit' => $this->config('limit', 5),
]);
}
}
Loading