mirror of https://github.com/Lissy93/dashy.git
✨ Adds refresh button to widget, for reloading data
This commit is contained in:
parent
ae8179ecd7
commit
2075cbc222
|
@ -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 |
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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}`, {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`, {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'] {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue