Implement new UI component for icons #230
- Introduce `AppIcon.vue`, offering improved performance over the previous `fort-awesome` dependency. This implementation reduces bundle size by 67.31KB (tested for web using `npm run build -- --mode prod`). - Migrate Font Awesome 5 icons to Font Awesome 6. This commit facilitates migration to Vue 3.0 (#230) and ensures no Vue component remains tightly bound to a specific Vue version, enhancing code portability. Font Awesome license is not included because Font Awesome revokes its right: > "Attribution is no longer required as of Font Awesome 3.0" > > Sources: > > - https://fontawesome.com/v4/license/ (archived: https://web.archive.org/web/20231003213441/https://fontawesome.com/v4/license/, https://archive.ph/Yy9j5) > - https://github.com/FortAwesome/Font-Awesome/wiki (archived: https://web.archive.org/web/20231003214646/https://github.com/FortAwesome/Font-Awesome/wiki, https://archive.ph/C6sXv) This commit removes following third-party production dependencies: - `@fortawesome/vue-fontawesome` - `@fortawesome/free-solid-svg-icons` - `@fortawesome/free-regular-svg-icons` - `@fortawesome/free-brands-svg-icons` - `@fortawesome/fontawesome-svg-core`
This commit is contained in:
@@ -4,30 +4,30 @@
|
||||
type="button"
|
||||
@click="onClicked"
|
||||
>
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="button__icon"
|
||||
:icon="[iconPrefix, iconName]"
|
||||
size="2x"
|
||||
:icon="iconName"
|
||||
/>
|
||||
<div class="button__text">{{text}}</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { IconName } from '@/presentation/components/Shared/Icon/IconName';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
AppIcon,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconPrefix: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
type: String as PropType<IconName>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
@@ -64,6 +64,10 @@ export default defineComponent({
|
||||
box-shadow: 0 3px 9px $color-primary-darkest;
|
||||
border-radius: 4px;
|
||||
|
||||
&__icon {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
@include clickable;
|
||||
|
||||
width: 10%;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<span class="dollar">$</span>
|
||||
<code><slot /></code>
|
||||
<TooltipWrapper>
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="copy-button"
|
||||
:icon="['fas', 'copy']"
|
||||
icon="copy"
|
||||
@click="copyCode"
|
||||
/>
|
||||
<template v-slot:tooltip>
|
||||
@@ -19,10 +19,12 @@
|
||||
import { defineComponent, useSlots } from 'vue';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TooltipWrapper,
|
||||
AppIcon,
|
||||
},
|
||||
setup() {
|
||||
const slots = useSlots();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<p>
|
||||
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
|
||||
steps. If you are unsure how to follow the instructions, hover on information
|
||||
(<font-awesome-icon :icon="['fas', 'info-circle']" />)
|
||||
(<AppIcon icon="circle-info" />)
|
||||
icons near the steps, or follow the easy alternative described above.
|
||||
</p>
|
||||
<p>
|
||||
@@ -27,9 +27,9 @@
|
||||
<div class="step__action">
|
||||
<span>{{ step.action.instruction }}</span>
|
||||
<TooltipWrapper v-if="step.action.details">
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
icon="circle-info"
|
||||
/>
|
||||
<template v-slot:tooltip>
|
||||
<div v-html="step.action.details" />
|
||||
@@ -39,9 +39,9 @@
|
||||
<div v-if="step.code" class="step__code">
|
||||
<CodeInstruction>{{ step.code.instruction }}</CodeInstruction>
|
||||
<TooltipWrapper v-if="step.code.details">
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
icon="circle-info"
|
||||
/>
|
||||
<template v-slot:tooltip>
|
||||
<div v-html="step.code.details" />
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import CodeInstruction from './CodeInstruction.vue';
|
||||
import { IInstructionListData } from './InstructionListData';
|
||||
|
||||
@@ -69,6 +70,7 @@ export default defineComponent({
|
||||
components: {
|
||||
CodeInstruction,
|
||||
TooltipWrapper,
|
||||
AppIcon,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
|
||||
@@ -4,19 +4,16 @@
|
||||
v-if="canRun"
|
||||
text="Run"
|
||||
v-on:click="executeCode"
|
||||
icon-prefix="fas"
|
||||
icon-name="play"
|
||||
/>
|
||||
<IconButton
|
||||
:text="isDesktopVersion ? 'Save' : 'Download'"
|
||||
v-on:click="saveCode"
|
||||
icon-prefix="fas"
|
||||
:icon-name="isDesktopVersion ? 'save' : 'file-download'"
|
||||
:icon-name="isDesktopVersion ? 'floppy-disk' : 'file-arrow-down'"
|
||||
/>
|
||||
<IconButton
|
||||
text="Copy"
|
||||
v-on:click="copyCode"
|
||||
icon-prefix="fas"
|
||||
icon-name="copy"
|
||||
/>
|
||||
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
:style="{ cursor: cursorCssValue }"
|
||||
@mousedown="startResize">
|
||||
<div class="line" />
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="icon"
|
||||
:icon="['fas', 'arrows-alt-h']"
|
||||
icon="left-right"
|
||||
/>
|
||||
<div class="line" />
|
||||
</div>
|
||||
@@ -14,8 +14,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onUnmounted } from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
AppIcon,
|
||||
},
|
||||
emits: {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
resized: (displacementX: number) => true,
|
||||
|
||||
@@ -17,18 +17,18 @@
|
||||
</span>
|
||||
<span v-else>Oh no 😢</span>
|
||||
<!-- Expand icon -->
|
||||
<font-awesome-icon
|
||||
<AppIcon
|
||||
class="card__inner__expand-icon"
|
||||
:icon="['far', isExpanded ? 'folder-open' : 'folder']"
|
||||
:icon="isExpanded ? 'folder-open' : 'folder'"
|
||||
/>
|
||||
<!-- Indeterminate and full states -->
|
||||
<div class="card__inner__state-icons">
|
||||
<font-awesome-icon
|
||||
:icon="['fa', 'battery-half']"
|
||||
<AppIcon
|
||||
icon="battery-half"
|
||||
v-if="isAnyChildSelected && !areAllChildrenSelected"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
:icon="['fa', 'battery-full']"
|
||||
<AppIcon
|
||||
icon="battery-full"
|
||||
v-if="areAllChildrenSelected"
|
||||
/>
|
||||
</div>
|
||||
@@ -38,8 +38,8 @@
|
||||
<ScriptsTree :categoryId="categoryId" />
|
||||
</div>
|
||||
<div class="card__expander__close-button">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
<AppIcon
|
||||
icon="xmark"
|
||||
v-on:click="collapse()"
|
||||
/>
|
||||
</div>
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
defineComponent, ref, watch, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||
@@ -59,6 +60,7 @@ import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ScriptsTree,
|
||||
AppIcon,
|
||||
},
|
||||
props: {
|
||||
categoryId: {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
class="search__query__close-button"
|
||||
v-on:click="clearSearchQuery()"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'times']" />
|
||||
<AppIcon icon="xmark" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
defineComponent, PropType, ref, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/Tree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||
@@ -52,6 +53,7 @@ export default defineComponent({
|
||||
components: {
|
||||
ScriptsTree,
|
||||
CardList,
|
||||
AppIcon,
|
||||
},
|
||||
props: {
|
||||
currentView: {
|
||||
|
||||
@@ -6,14 +6,18 @@
|
||||
v-on:click.stop
|
||||
v-on:click="toggle()"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||
<AppIcon icon="circle-info" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
AppIcon,
|
||||
},
|
||||
emits: [
|
||||
'show',
|
||||
'hide',
|
||||
@@ -52,5 +56,4 @@ export default defineComponent({
|
||||
color: $color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
40
src/presentation/components/Shared/Icon/AppIcon.vue
Normal file
40
src/presentation/components/Shared/Icon/AppIcon.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div v-html="svgContent" class="inline-icon" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useSvgLoader } from './UseSvgLoader';
|
||||
import { IconName } from './IconName';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
icon: {
|
||||
type: String as PropType<IconName>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const useSvgLoaderHook = inject('useSvgLoaderHook', useSvgLoader);
|
||||
const { svgContent } = useSvgLoaderHook(() => props.icon);
|
||||
return { svgContent };
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.inline-icon {
|
||||
display: inline-block;
|
||||
::v-deep svg { // using ::v-deep because when v-html is used the content doesn't go through Vue's template compiler.
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
overflow: visible;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/presentation/components/Shared/Icon/IconName.ts
Normal file
22
src/presentation/components/Shared/Icon/IconName.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const IconNames = [
|
||||
'magnifying-glass',
|
||||
'copy',
|
||||
'circle-info',
|
||||
'user-secret',
|
||||
'tag',
|
||||
'github',
|
||||
'face-smile',
|
||||
'globe',
|
||||
'desktop',
|
||||
'xmark',
|
||||
'battery-half',
|
||||
'battery-full',
|
||||
'folder',
|
||||
'folder-open',
|
||||
'left-right',
|
||||
'file-arrow-down',
|
||||
'floppy-disk',
|
||||
'play',
|
||||
] as const;
|
||||
|
||||
export type IconName = typeof IconNames[number];
|
||||
92
src/presentation/components/Shared/Icon/UseSvgLoader.ts
Normal file
92
src/presentation/components/Shared/Icon/UseSvgLoader.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
WatchSource, readonly, ref, watch,
|
||||
} from 'vue';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { IconName } from './IconName';
|
||||
|
||||
export function useSvgLoader(
|
||||
iconWatcher: WatchSource<IconName>,
|
||||
loaders: FileLoaders = RawSvgLoaders,
|
||||
) {
|
||||
const svgContent = ref<string>('');
|
||||
|
||||
watch(iconWatcher, async (iconName) => {
|
||||
svgContent.value = await lazyLoadSvg(iconName, loaders);
|
||||
}, { immediate: true });
|
||||
|
||||
return {
|
||||
svgContent: readonly(svgContent),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearIconCache() {
|
||||
LazyIconCache.clear();
|
||||
}
|
||||
|
||||
export type FileLoaders = Record<string, () => Promise<string>>;
|
||||
|
||||
const LazyIconCache = new Map<IconName, AsyncLazy<string>>();
|
||||
|
||||
async function lazyLoadSvg(name: IconName, loaders: FileLoaders): Promise<string> {
|
||||
let iconLoader = LazyIconCache.get(name);
|
||||
if (!iconLoader) {
|
||||
iconLoader = new AsyncLazy<string>(() => loadSvg(name, loaders));
|
||||
LazyIconCache.set(name, iconLoader);
|
||||
}
|
||||
const icon = await iconLoader.getValue();
|
||||
return icon;
|
||||
}
|
||||
|
||||
async function loadSvg(name: IconName, loaders: FileLoaders): Promise<string> {
|
||||
const iconPath = `/assets/icons/${name}.svg`;
|
||||
const loader = loaders[iconPath];
|
||||
if (!loader) {
|
||||
throw new Error(`missing icon for "${name}" in "${iconPath}"`);
|
||||
}
|
||||
const svgContent = await loader();
|
||||
const modifiedContent = modifySvg(svgContent);
|
||||
return modifiedContent;
|
||||
}
|
||||
|
||||
const RawSvgLoaders = import.meta.glob('@/presentation/assets/icons/**/*.svg', {
|
||||
as: 'raw', // This will load the SVG file content as a string.
|
||||
/*
|
||||
Using `eager: true` to preload all icons.
|
||||
Pros:
|
||||
- Speed: Icons are instantly accessible post-initial load.
|
||||
Cons:
|
||||
- Increased initial load time due to preloading of all icons.
|
||||
- Increased bundle size.
|
||||
*/
|
||||
eager: false,
|
||||
});
|
||||
|
||||
function modifySvg(svgSource: string): string {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgSource, 'image/svg+xml');
|
||||
let svgRoot = doc.documentElement;
|
||||
svgRoot = removeSvgComments(svgRoot);
|
||||
svgRoot = fillSvgCurrentColor(svgRoot);
|
||||
return new XMLSerializer()
|
||||
.serializeToString(svgRoot);
|
||||
}
|
||||
|
||||
function removeSvgComments(svgRoot: HTMLElement): HTMLElement {
|
||||
const comments = Array.from(svgRoot.childNodes).filter(
|
||||
(node) => node.nodeType === Node.COMMENT_NODE,
|
||||
);
|
||||
for (const comment of comments) {
|
||||
svgRoot.removeChild(comment);
|
||||
}
|
||||
Array.from(svgRoot.children).forEach((child) => {
|
||||
removeSvgComments(child as HTMLElement);
|
||||
});
|
||||
return svgRoot;
|
||||
}
|
||||
|
||||
function fillSvgCurrentColor(svgRoot: HTMLElement): HTMLElement {
|
||||
svgRoot.querySelectorAll('path').forEach((el: Element) => {
|
||||
el.setAttribute('fill', 'currentColor');
|
||||
});
|
||||
return svgRoot;
|
||||
}
|
||||
@@ -10,9 +10,7 @@
|
||||
class="dialog__close-button"
|
||||
@click="hide"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
/>
|
||||
<AppIcon icon="xmark" />
|
||||
</div>
|
||||
</div>
|
||||
</ModalContainer>
|
||||
@@ -20,11 +18,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import ModalContainer from './ModalContainer.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ModalContainer,
|
||||
AppIcon,
|
||||
},
|
||||
emits: {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'container-supported': hasCurrentOsDesktopVersion,
|
||||
}">
|
||||
<span class="description">
|
||||
<font-awesome-icon class="description__icon" :icon="['fas', 'desktop']" />
|
||||
<AppIcon class="description__icon" icon="desktop" />
|
||||
<span class="description__text">For desktop:</span>
|
||||
</span>
|
||||
<span class="urls">
|
||||
@@ -21,6 +21,7 @@
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||
|
||||
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
||||
@@ -32,6 +33,7 @@ const supportedOperativeSystems: readonly OperatingSystem[] = [
|
||||
export default defineComponent({
|
||||
components: {
|
||||
DownloadUrlListItem,
|
||||
AppIcon,
|
||||
},
|
||||
setup() {
|
||||
const { os: currentOs } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="footer">
|
||||
<div class="footer__section">
|
||||
<span v-if="isDesktop" class="footer__section__item">
|
||||
<font-awesome-icon class="icon" :icon="['fas', 'globe']" />
|
||||
<AppIcon class="icon" icon="globe" />
|
||||
<span>
|
||||
Online version at <a :href="homepageUrl" target="_blank" rel="noopener noreferrer">{{ homepageUrl }}</a>
|
||||
</span>
|
||||
@@ -15,24 +15,24 @@
|
||||
<div class="footer__section">
|
||||
<div class="footer__section__item">
|
||||
<a :href="feedbackUrl" target="_blank" rel="noopener noreferrer">
|
||||
<font-awesome-icon class="icon" :icon="['far', 'smile']" />
|
||||
<AppIcon class="icon" icon="face-smile" />
|
||||
<span>Feedback</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer__section__item">
|
||||
<a :href="repositoryUrl" target="_blank" rel="noopener noreferrer">
|
||||
<font-awesome-icon class="icon" :icon="['fab', 'github']" />
|
||||
<AppIcon class="icon" icon="github" />
|
||||
<span>Source Code</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer__section__item">
|
||||
<a :href="releaseUrl" target="_blank" rel="noopener noreferrer">
|
||||
<font-awesome-icon class="icon" :icon="['fas', 'tag']" />
|
||||
<AppIcon class="icon" icon="tag" />
|
||||
<span>v{{ version }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer__section__item">
|
||||
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
|
||||
<AppIcon class="icon" icon="user-secret" />
|
||||
<a @click="showPrivacyDialog()">Privacy</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
defineComponent, ref, computed, inject,
|
||||
} from 'vue';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import DownloadUrlList from './DownloadUrlList.vue';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
@@ -57,6 +58,7 @@ export default defineComponent({
|
||||
ModalDialog,
|
||||
PrivacyPolicy,
|
||||
DownloadUrlList,
|
||||
AppIcon,
|
||||
},
|
||||
setup() {
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
v-model="searchQuery"
|
||||
>
|
||||
<div class="icon-wrapper">
|
||||
<font-awesome-icon :icon="['fas', 'search']" />
|
||||
<AppIcon icon="magnifying-glass" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,11 +19,13 @@ import {
|
||||
} from 'vue';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
export default defineComponent({
|
||||
components: { AppIcon },
|
||||
directives: {
|
||||
NonCollapsing,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user