Adds refresh button to widget, for reloading data

This commit is contained in:
Alicia Sykes 2021-12-13 16:22:24 +00:00
parent ae8179ecd7
commit 2075cbc222
18 changed files with 254 additions and 49 deletions

View File

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="sync" class="svg-inline--fa fa-sync fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M500 8h-27.711c-6.739 0-12.157 5.548-11.997 12.286l2.347 98.575C418.212 52.043 342.256 8 256 8 134.813 8 33.933 94.924 12.296 209.824 10.908 217.193 16.604 224 24.103 224h28.576c5.674 0 10.542-3.982 11.737-9.529C83.441 126.128 161.917 60 256 60c79.545 0 147.942 47.282 178.676 115.302l-126.39-3.009c-6.737-.16-12.286 5.257-12.286 11.997V212c0 6.627 5.373 12 12 12h192c6.627 0 12-5.373 12-12V20c0-6.627-5.373-12-12-12zm-12.103 280h-28.576c-5.674 0-10.542 3.982-11.737 9.529C428.559 385.872 350.083 452 256 452c-79.546 0-147.942-47.282-178.676-115.302l126.39 3.009c6.737.16 12.286-5.257 12.286-11.997V300c0-6.627-5.373-12-12-12H12c-6.627 0-12 5.373-12 12v192c0 6.627 5.373 12 12 12h27.711c6.739 0 12.157-5.548 11.997-12.286l-2.347-98.575C93.788 459.957 169.744 504 256 504c121.187 0 222.067-86.924 243.704-201.824 1.388-7.369-4.308-14.176-11.807-14.176z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -12,11 +12,11 @@
@openContextMenu="openContextMenu"
>
<!-- If no items, show message -->
<div v-if="(!items || items.length < 1) && !isEditMode" class="no-items">
<div v-if="sectionType === 'empty'" class="no-items">
{{ $t('home.no-items-section') }}
</div>
<!-- Item Container -->
<div v-else
<div v-else-if="sectionType === 'item'"
:class="`there-are-items ${isGridLayout? 'item-group-grid': ''} inner-size-${itemSize}`"
:style="gridStyle" :id="`section-${groupId}`"
> <!-- Show for each item -->
@ -57,6 +57,14 @@
:itemSize="itemSize"
/>
</div>
<div v-else-if="sectionType === 'widget'">
<WidgetBase
v-for="(widget, widgetIndx) in widgets"
:key="widgetIndx"
:widget="widget"
:index="index"
/>
</div>
<!-- Modal for opening in modal view -->
<IframeModal
:ref="`iframeModal-${groupId}`"
@ -87,6 +95,7 @@
<script>
import router from '@/router';
import Item from '@/components/LinkItems/Item.vue';
import WidgetBase from '@/components/Widgets/WidgetBase';
import Collapsable from '@/components/LinkItems/Collapsable.vue';
import IframeModal from '@/components/LinkItems/IframeModal.vue';
import EditSection from '@/components/InteractiveEditor/EditSection.vue';
@ -107,12 +116,14 @@ export default {
icon: String,
displayData: Object,
items: Array,
widgets: Array,
index: Number,
},
components: {
Collapsable,
ContextMenu,
Item,
WidgetBase,
IframeModal,
EditSection,
},
@ -139,6 +150,12 @@ export default {
sortOrder() {
return this.displayData.sortBy || defaultSortOrder;
},
/* A section can contain either items or widgets */
sectionType() {
if (this.widgets && this.widgets.length > 0) return 'widget';
if (this.items && this.items.length > 0) return 'item';
return 'empty';
},
/* If the sortBy attribute is specified, then return sorted data */
sortedItems() {
let { items } = this;

View File

@ -39,8 +39,12 @@ export default {
},
},
methods: {
update() {
this.setTime();
this.setDate();
},
/* Get and format the current time */
updateTime() {
setTime() {
this.time = Intl.DateTimeFormat(this.timeFormat, {
timeZone: this.timeZone,
hour: 'numeric',
@ -49,7 +53,7 @@ export default {
}).format();
},
/* Get and format the date */
updateDate() {
setDate() {
this.date = new Date().toLocaleDateString(this.timeFormat, {
weekday: 'long', day: 'numeric', year: 'numeric', month: 'short',
});
@ -57,14 +61,13 @@ export default {
},
created() {
// Set initial date and time
this.updateTime();
this.updateDate();
this.update();
// Update the date every hour, and the time each second
this.timeUpdateInterval = setInterval(() => {
this.updateTime();
this.setTime();
const now = new Date();
if (now.getMinutes() === 0 && now.getSeconds() === 0) {
this.updateDate();
this.setDate();
}
}, 1000);
},

View File

@ -65,6 +65,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Create new chart, using the crypto data */
generateChart() {
return new Chart(`#${this.chartId}`, {

View File

@ -75,6 +75,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@ -52,6 +52,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@ -1,6 +1,6 @@
<template>
<div class="iframe-widget">
<iframe v-if="frameUrl" :src="frameUrl" />
<iframe v-if="frameUrl" :src="frameUrl" :id="frameId" />
</div>
</template>
@ -11,6 +11,7 @@ import ErrorHandler from '@/utils/ErrorHandler';
export default {
mixins: [WidgetMixin],
computed: {
/* Gets users specified URL to load into the iframe */
frameUrl() {
const usersChoice = this.options.url;
if (!usersChoice || typeof usersChoice !== 'string') {
@ -19,6 +20,16 @@ export default {
}
return usersChoice;
},
/* Generates an ID for the iframe */
frameId() {
return `iframe-${btoa(this.frameUrl || 'empty').substring(0, 16)}`;
},
},
methods: {
/* Refreshes iframe contents, called by parent */
update() {
(document.getElementById(this.frameId) || {}).src = this.frameUrl;
},
},
};
</script>

View File

@ -54,6 +54,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to Jokes API endpoint */
fetchData() {
axios.get(this.endpoint)

View File

@ -72,6 +72,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Create new chart, using the crypto data */
generateChart() {
return new Chart(`#${this.chartId}`, {

View File

@ -50,6 +50,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Makes GET request to the TFL API */
fetchData() {
axios.get(widgetApiEndpoints.tflStatus)

View File

@ -1,5 +1,6 @@
<template>
<div class="weather">
<LoadingAnimation v-if="loading" class="loader" />
<div v-else class="weather">
<!-- Icon + Temperature -->
<div class="intro">
<p class="temp">{{ temp }}</p>
@ -65,6 +66,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
processTemp(temp) {
return `${Math.round(temp)}${this.tempDisplayUnits}`;
@ -73,6 +78,7 @@ export default {
fetchWeather() {
axios.get(this.endpoint)
.then((response) => {
this.loading = false;
const { data } = response;
this.icon = data.weather[0].icon;
this.description = data.weather[0].description;
@ -141,6 +147,10 @@ export default {
<style scoped lang="scss">
@import '@/styles/weather-icons.scss';
.loader {
margin: 0 auto;
display: flex;
}
p {
color: var(--widget-text-color);
}

View File

@ -75,6 +75,10 @@ export default {
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchWeather();
},
/* Adds units symbol to temperature, depending on metric or imperial */
processTemp(temp) {
return `${Math.round(temp)}${this.tempDisplayUnits}`;

View File

@ -1,19 +1,71 @@
<template>
<Clock v-if="widgetType === 'clock'" :options="widgetOptions" />
<Weather v-else-if="widgetType === 'weather'" :options="widgetOptions" />
<WeatherForecast v-else-if="widgetType === 'weather-forecast'" :options="widgetOptions" />
<TflStatus v-else-if="widgetType === 'tfl-status'" :options="widgetOptions" />
<CryptoPriceChart v-else-if="widgetType === 'crypto-price-chart'" :options="widgetOptions" />
<CryptoWatchList v-else-if="widgetType === 'crypto-watch-list'" :options="widgetOptions" />
<XkcdComic v-else-if="widgetType === 'xkcd-comic'" :options="widgetOptions" />
<ExchangeRates v-else-if="widgetType === 'exchange-rates'" :options="widgetOptions" />
<StockPriceChart v-else-if="widgetType === 'stock-price-chart'" :options="widgetOptions" />
<Jokes v-else-if="widgetType === 'joke'" :options="widgetOptions" />
<IframeWidget v-else-if="widgetType === 'iframe'" :options="widgetOptions" />
<div class="widget-base">
<Button :click="update" class="update-btn">
<UpdateIcon />
</Button>
<Clock
v-if="widgetType === 'clock'"
:options="widgetOptions"
:ref="widgetRef"
/>
<Weather
v-else-if="widgetType === 'weather'"
:options="widgetOptions"
:ref="widgetRef"
/>
<WeatherForecast
v-else-if="widgetType === 'weather-forecast'"
:options="widgetOptions"
:ref="widgetRef"
/>
<TflStatus
v-else-if="widgetType === 'tfl-status'"
:options="widgetOptions"
:ref="widgetRef"
/>
<CryptoPriceChart
v-else-if="widgetType === 'crypto-price-chart'"
:options="widgetOptions"
:ref="widgetRef"
/>
<CryptoWatchList
v-else-if="widgetType === 'crypto-watch-list'"
:options="widgetOptions"
:ref="widgetRef"
/>
<XkcdComic
v-else-if="widgetType === 'xkcd-comic'"
:options="widgetOptions"
:ref="widgetRef"
/>
<ExchangeRates
v-else-if="widgetType === 'exchange-rates'"
:options="widgetOptions"
:ref="widgetRef"
/>
<StockPriceChart
v-else-if="widgetType === 'stock-price-chart'"
:options="widgetOptions"
:ref="widgetRef"
/>
<Jokes
v-else-if="widgetType === 'joke'"
:options="widgetOptions"
:ref="widgetRef"
/>
<IframeWidget
v-else-if="widgetType === 'iframe'"
:options="widgetOptions"
:ref="widgetRef"
/>
</div>
</template>
<script>
import ErrorHandler from '@/utils/ErrorHandler';
import Button from '@/components/FormElements/Button';
import UpdateIcon from '@/assets/interface-icons/widget-update.svg';
import Clock from '@/components/Widgets/Clock.vue';
import Weather from '@/components/Widgets/Weather.vue';
import WeatherForecast from '@/components/Widgets/WeatherForecast.vue';
@ -29,6 +81,8 @@ import IframeWidget from '@/components/Widgets/IframeWidget.vue';
export default {
name: 'Widget',
components: {
Button,
UpdateIcon,
Clock,
Weather,
WeatherForecast,
@ -46,9 +100,7 @@ export default {
index: Number,
},
computed: {
groupId() {
return `widget-${this.index}`;
},
/* Returns the widget type, shows error if not specified */
widgetType() {
if (!this.widget.type) {
ErrorHandler('Missing type attribute for widget');
@ -56,14 +108,44 @@ export default {
}
return this.widget.type.toLowerCase();
},
/* Returns the users specified widget options, or empty object */
widgetOptions() {
return this.widget.options || {};
},
widgetRef() {
return `widget-${this.widgetType}-${this.index}`;
},
},
methods: {
update() {
this.$refs[this.widgetRef].update();
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
.widget-base {
position: relative;
padding-top: 0.75rem;
button.update-btn {
height: 1.5rem;
min-width: auto;
width: 2rem;
margin: 0;
padding: 0.1rem 0;
position: absolute;
right: -0.25rem;
top: -0.25rem;
border: none;
opacity: var(--dimming-factor);
color: var(--widget-text-color);
&:hover {
opacity: 1;
color: var(--widget-background-color);
}
}
}
</style>

View File

@ -0,0 +1,18 @@
<template>
<div class="widget-error"></div>
</template>
<script>
export default {
name: 'WidgetError',
props: {
errorMessage: String,
},
computed: {},
};
</script>
<style scoped lang="scss">
.widget-error {}
</style>

View File

@ -40,11 +40,18 @@ export default {
}
return 'latest';
},
endpoint() {
return `${widgetApiEndpoints.xkcdComic}?comic=${this.comicNumber}`;
},
},
methods: {
/* Extends mixin, and updates data. Called by parent component */
update() {
this.fetchData();
},
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(`${widgetApiEndpoints.xkcdComic}?comic=${this.comicNumber}`)
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data);
})

View File

@ -1,10 +1,31 @@
import LoadingAnimation from '@/assets/interface-icons/loader.svg';
const WidgetMixin = {
components: {
LoadingAnimation,
},
props: {
/* The options prop is an object of settings for a given widget */
options: {
type: Object,
default: {},
},
},
data: () => ({
loading: true, // Indicates current loading status, to display spinner
}),
methods: {
/* Overridden by widget component. Re-fetches and renders any external data *
* Called by parent component, and triggered either by user or time interval */
update() {
// eslint-disable-next-line no-console
console.log('No update method configured for this widget');
},
},
mounted() {
// If the mounted function isn't overridden,then hide loader
this.loading = false;
},
};
export default WidgetMixin;

View File

@ -331,6 +331,9 @@ html[data-theme='colorful'] {
div.context-menu {
border-color: var(--primary);
}
.collapsable.is-open {
height: -webkit-fill-available;
}
}
html[data-theme='minimal-light'], html[data-theme='minimal-dark'], html[data-theme='vaporware'] {

View File

@ -28,23 +28,25 @@
+ (this.colCount ? `col-count-${this.colCount} ` : '')"
>
<!-- Display any dynamic widget content -->
<WidgetGroup v-if="!singleSectionView" :widgets="widgets" />
<Section
v-for="(section, index) in filteredTiles"
:key="index"
:index="index"
:title="section.name"
:icon="section.icon || undefined"
:displayData="getDisplayData(section)"
:groupId="`section-${index}`"
:items="filterTiles(section.items, searchValue)"
:searchTerm="searchValue"
:itemSize="itemSizeBound"
@itemClicked="finishedSearching()"
@change-modal-visibility="updateModalVisibility"
:class="
(searchValue && filterTiles(section.items, searchValue).length === 0) ? 'no-results' : ''"
/>
<!-- <WidgetGroup v-if="!singleSectionView" :widgets="widgets" /> -->
<template v-for="(section, index) in filteredTiles">
<Section
:key="index"
:index="index"
:title="section.name"
:icon="section.icon || undefined"
:displayData="getDisplayData(section)"
:groupId="`section-${index}`"
:items="filterTiles(section.items, searchValue)"
:widgets="section.widgets"
:searchTerm="searchValue"
:itemSize="itemSizeBound"
@itemClicked="finishedSearching()"
@change-modal-visibility="updateModalVisibility"
:class="
(searchValue && filterTiles(section.items, searchValue).length === 0) ? 'no-results' : ''"
/>
</template>
<!-- Show add new section button, in edit mode -->
<AddNewSection v-if="isEditMode" />
</div>
@ -63,7 +65,7 @@
import SettingsContainer from '@/components/Settings/SettingsContainer.vue';
import Section from '@/components/LinkItems/Section.vue';
import WidgetGroup from '@/components/Widgets/WidgetGroup';
// import WidgetGroup from '@/components/Widgets/WidgetGroup';
import EditModeSaveMenu from '@/components/InteractiveEditor/EditModeSaveMenu.vue';
import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vue';
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
@ -77,7 +79,7 @@ export default {
name: 'home',
components: {
SettingsContainer,
WidgetGroup,
// WidgetGroup,
EditModeSaveMenu,
ExportConfigMenu,
AddNewSection,
@ -160,7 +162,7 @@ export default {
},
/* Returns only the tiles that match the users search query */
filterTiles(allTiles, searchTerm) {
return searchTiles(allTiles, searchTerm);
return searchTiles(allTiles, searchTerm) || [];
},
/* Returns optional section display preferences if available */
getDisplayData(section) {
@ -216,10 +218,12 @@ export default {
let isNeeded = false;
if (!this.sections) return false;
this.sections.forEach((section) => {
if (section.icon && section.icon.includes(prefix)) isNeeded = true;
section.items.forEach((item) => {
if (item.icon && item.icon.includes(prefix)) isNeeded = true;
});
if (section && section.items) {
if (section.icon && section.icon.includes(prefix)) isNeeded = true;
section.items.forEach((item) => {
if (item.icon && item.icon.includes(prefix)) isNeeded = true;
});
}
});
return isNeeded;
},