Fix memory leaks via auto-unsubscribing and DI
This commit simplifies event handling, providing a unified and robust
way to handle event lifecycling. This way, it fixes events not being
unsubscribed when state is changed.
Introduce a new function in `EventSubscriptionCollection` to remove
existing events and adding new events. This provides an easier to use
API, which leads to code that's easier to understand. It also prevents
potential bugs that may occur due to forgetting to call both functions.
It fixes `TheScriptsMenu` not unregistering events on state change.
Other improvements include:
- Include a getter to get total amount of registered subcriptions.
This helps in unit testing.
- Have nullish checks to prevent potential errors further down the
execution.
- Use array instead of rest parameters to increase readability and
simplify tests.
Ensure `SliderHandler` stops resizes on unmount, unsubscribing from all
events and resetting state to default.
Update `injectionKeys` to do imports as types to avoid circular
dependencies. Simplify importing `injectionKeys` to enable and strict
typings for iterating injection keys.
Add tests covering new behavior.
This commit is contained in:
@@ -59,7 +59,7 @@ import {
|
||||
defineComponent, PropType, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||
import CodeInstruction from './CodeInstruction.vue';
|
||||
@@ -77,7 +77,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const appName = computed<string>(() => info.name);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
import {
|
||||
defineComponent, ref, computed, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
@@ -53,9 +53,10 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
currentState, currentContext, onStateChange, events,
|
||||
} = inject(useCollectionStateKey)();
|
||||
const { os, isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||
currentState, currentContext, onStateChange,
|
||||
} = inject(InjectionKeys.useCollectionState)();
|
||||
const { os, isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const canRun = computed<boolean>(() => getCanRunState(currentState.value.os, isDesktop, os));
|
||||
@@ -81,15 +82,18 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
onStateChange((newState) => {
|
||||
updateCurrentCode(newState.code.current);
|
||||
subscribeToCodeChanges(newState.code);
|
||||
}, { immediate: true });
|
||||
|
||||
function subscribeToCodeChanges(code: IApplicationCode) {
|
||||
hasCode.value = code.current && code.current.length > 0;
|
||||
events.unsubscribeAll();
|
||||
events.register(code.changed.on((newCode) => {
|
||||
hasCode.value = newCode && newCode.code.length > 0;
|
||||
}));
|
||||
events.unsubscribeAllAndRegister([
|
||||
code.changed.on((newCode) => updateCurrentCode(newCode.code)),
|
||||
]);
|
||||
}
|
||||
|
||||
function updateCurrentCode(code: string) {
|
||||
hasCode.value = code && code.length > 0;
|
||||
}
|
||||
|
||||
async function getCurrentCode(): Promise<IApplicationCode> {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import {
|
||||
defineComponent, onUnmounted, onMounted, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
@@ -37,7 +37,8 @@ export default defineComponent({
|
||||
NonCollapsing,
|
||||
},
|
||||
setup(props) {
|
||||
const { onStateChange, currentState, events } = inject(useCollectionStateKey)();
|
||||
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const editorId = 'codeEditor';
|
||||
let editor: ace.Ace.Editor | undefined;
|
||||
@@ -61,19 +62,20 @@ export default defineComponent({
|
||||
newState.collection.scripting.language,
|
||||
);
|
||||
const appCode = newState.code;
|
||||
const innerCode = appCode.current || getDefaultCode(newState.collection.scripting.language);
|
||||
editor.setValue(innerCode, 1);
|
||||
events.unsubscribeAll();
|
||||
events.register(appCode.changed.on((code) => updateCode(code)));
|
||||
updateCode(appCode.current, newState.collection.scripting.language);
|
||||
events.unsubscribeAllAndRegister([
|
||||
appCode.changed.on((code) => handleCodeChange(code)),
|
||||
]);
|
||||
}
|
||||
|
||||
function updateCode(event: ICodeChangedEvent) {
|
||||
function updateCode(code: string, language: ScriptingLanguage) {
|
||||
const innerCode = code || getDefaultCode(language);
|
||||
editor.setValue(innerCode, 1);
|
||||
}
|
||||
|
||||
function handleCodeChange(event: ICodeChangedEvent) {
|
||||
removeCurrentHighlighting();
|
||||
if (event.isEmpty()) {
|
||||
const defaultCode = getDefaultCode(currentState.value.collection.scripting.language);
|
||||
editor.setValue(defaultCode, 1);
|
||||
return;
|
||||
}
|
||||
updateCode(event.code, currentState.value.collection.scripting.language);
|
||||
editor.setValue(event.code, 1);
|
||||
if (event.addedScripts?.length > 0) {
|
||||
reactToChanges(event, event.addedScripts);
|
||||
|
||||
@@ -66,9 +66,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject } from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
import MenuOptionList from '../MenuOptionList.vue';
|
||||
import MenuOptionListItem from '../MenuOptionListItem.vue';
|
||||
import { SelectionType, SelectionTypeHandler } from './SelectionTypeHandler';
|
||||
@@ -80,28 +81,27 @@ export default defineComponent({
|
||||
TooltipWrapper,
|
||||
},
|
||||
setup() {
|
||||
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
||||
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const currentSelection = ref(SelectionType.None);
|
||||
|
||||
let selectionTypeHandler: SelectionTypeHandler;
|
||||
|
||||
onStateChange(() => {
|
||||
unregisterMutators();
|
||||
|
||||
modifyCurrentState((state) => {
|
||||
registerStateMutator(state);
|
||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
||||
updateSelections();
|
||||
events.unsubscribeAllAndRegister([
|
||||
subscribeAndUpdateSelections(state),
|
||||
]);
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
function unregisterMutators() {
|
||||
events.unsubscribeAll();
|
||||
}
|
||||
|
||||
function registerStateMutator(state: ICategoryCollectionState) {
|
||||
selectionTypeHandler = new SelectionTypeHandler(state);
|
||||
updateSelections();
|
||||
events.register(state.selection.changed.on(() => updateSelections()));
|
||||
function subscribeAndUpdateSelections(
|
||||
state: ICategoryCollectionState,
|
||||
): IEventSubscription {
|
||||
return state.selection.changed.on(() => updateSelections());
|
||||
}
|
||||
|
||||
function selectType(type: SelectionType) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import {
|
||||
defineComponent, computed, inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import MenuOptionList from './MenuOptionList.vue';
|
||||
import MenuOptionListItem from './MenuOptionListItem.vue';
|
||||
@@ -30,8 +30,8 @@ export default defineComponent({
|
||||
MenuOptionListItem,
|
||||
},
|
||||
setup() {
|
||||
const { modifyCurrentContext, currentState } = inject(useCollectionStateKey)();
|
||||
const { application } = inject(useApplicationKey);
|
||||
const { modifyCurrentContext, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||
const { application } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
||||
application.getSupportedOsList() ?? [])
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
defineComponent, ref, onUnmounted, inject,
|
||||
defineComponent, ref, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { IReadOnlyUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
import TheOsChanger from './TheOsChanger.vue';
|
||||
import TheSelector from './Selector/TheSelector.vue';
|
||||
import TheViewChanger from './View/TheViewChanger.vue';
|
||||
@@ -26,31 +27,26 @@ export default defineComponent({
|
||||
TheViewChanger,
|
||||
},
|
||||
setup() {
|
||||
const { onStateChange, events } = inject(useCollectionStateKey)();
|
||||
const { onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const isSearching = ref(false);
|
||||
|
||||
onStateChange((state) => {
|
||||
subscribeToFilterChanges(state.filter);
|
||||
events.unsubscribeAllAndRegister([
|
||||
subscribeToFilterChanges(state.filter),
|
||||
]);
|
||||
}, { immediate: true });
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeAll();
|
||||
});
|
||||
|
||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||
events.register(
|
||||
filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: () => { isSearching.value = true; },
|
||||
onClear: () => { isSearching.value = false; },
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function unsubscribeAll() {
|
||||
events.unsubscribeAll();
|
||||
function subscribeToFilterChanges(
|
||||
filter: IReadOnlyUserFilter,
|
||||
): IEventSubscription {
|
||||
return filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: () => { isSearching.value = true; },
|
||||
onClear: () => { isSearching.value = false; },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, onUnmounted } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
emits: {
|
||||
@@ -46,6 +46,10 @@ export default defineComponent({
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopResize();
|
||||
});
|
||||
|
||||
return {
|
||||
cursorCssValue,
|
||||
startResize,
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
defineComponent, ref, onMounted, onUnmounted, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue';
|
||||
import { hasDirective } from './NonCollapsingDirective';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
@@ -48,7 +48,7 @@ export default defineComponent({
|
||||
SizeObserver,
|
||||
},
|
||||
setup() {
|
||||
const { currentState, onStateChange } = inject(useCollectionStateKey)();
|
||||
const { currentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||
|
||||
const width = ref<number>(0);
|
||||
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
defineComponent, ref, watch, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
||||
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
||||
|
||||
@@ -76,7 +76,8 @@ export default defineComponent({
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { events, onStateChange, currentState } = inject(useCollectionStateKey)();
|
||||
const { onStateChange, currentState } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const isExpanded = computed({
|
||||
get: () => {
|
||||
@@ -106,12 +107,13 @@ export default defineComponent({
|
||||
isExpanded.value = false;
|
||||
}
|
||||
|
||||
onStateChange(async (state) => {
|
||||
events.unsubscribeAll();
|
||||
events.register(state.selection.changed.on(
|
||||
() => updateSelectionIndicators(props.categoryId),
|
||||
));
|
||||
await updateSelectionIndicators(props.categoryId);
|
||||
onStateChange((state) => {
|
||||
events.unsubscribeAllAndRegister([
|
||||
state.selection.changed.on(
|
||||
() => updateSelectionIndicators(props.categoryId),
|
||||
),
|
||||
]);
|
||||
updateSelectionIndicators(props.categoryId);
|
||||
}, { immediate: true });
|
||||
|
||||
watch(
|
||||
@@ -124,7 +126,7 @@ export default defineComponent({
|
||||
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function updateSelectionIndicators(categoryId: number) {
|
||||
function updateSelectionIndicators(categoryId: number) {
|
||||
const category = currentState.value.collection.findCategory(categoryId);
|
||||
const { selection } = currentState.value;
|
||||
isAnyChildSelected.value = category ? selection.isAnySelected(category) : false;
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
import {
|
||||
defineComponent, watch, ref, inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
import {
|
||||
parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId,
|
||||
getScriptId,
|
||||
@@ -43,8 +44,9 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
modifyCurrentState, currentState, onStateChange, events,
|
||||
} = inject(useCollectionStateKey)();
|
||||
modifyCurrentState, currentState, onStateChange,
|
||||
} = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const nodes = ref<ReadonlyArray<INodeContent>>([]);
|
||||
const selectedNodeIds = ref<ReadonlyArray<string>>([]);
|
||||
@@ -54,7 +56,7 @@ export default defineComponent({
|
||||
|
||||
watch(
|
||||
() => props.categoryId,
|
||||
async (newCategoryId) => { await setNodes(newCategoryId); },
|
||||
(newCategoryId) => setNodes(newCategoryId),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
@@ -63,8 +65,7 @@ export default defineComponent({
|
||||
if (!props.categoryId) {
|
||||
nodes.value = parseAllCategories(state.collection);
|
||||
}
|
||||
events.unsubscribeAll();
|
||||
subscribeToState(state);
|
||||
events.unsubscribeAllAndRegister(subscribeToState(state));
|
||||
}, { immediate: true });
|
||||
|
||||
function toggleNodeSelection(event: INodeSelectedEvent) {
|
||||
@@ -87,7 +88,7 @@ export default defineComponent({
|
||||
|| containsCategory(node, filtered.categoryMatches);
|
||||
}
|
||||
|
||||
async function setNodes(categoryId?: number) {
|
||||
function setNodes(categoryId?: number) {
|
||||
if (categoryId) {
|
||||
nodes.value = parseSingleCategory(categoryId, currentState.value.collection);
|
||||
} else {
|
||||
@@ -97,8 +98,10 @@ export default defineComponent({
|
||||
.map((selected) => getScriptNodeId(selected.script));
|
||||
}
|
||||
|
||||
function subscribeToState(state: IReadOnlyCategoryCollectionState) {
|
||||
events.register(
|
||||
function subscribeToState(
|
||||
state: IReadOnlyCategoryCollectionState,
|
||||
): IEventSubscription[] {
|
||||
return [
|
||||
state.selection.changed.on((scripts) => handleSelectionChanged(scripts)),
|
||||
state.filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
@@ -111,7 +114,7 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
function setCurrentFilter(currentFilter: IFilterResult | undefined) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
computed, inject,
|
||||
} from 'vue';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { IReverter } from './Reverter/IReverter';
|
||||
import { INodeContent } from './INodeContent';
|
||||
import { getReverter } from './Reverter/ReverterFactory';
|
||||
@@ -30,8 +30,9 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const {
|
||||
currentState, modifyCurrentState, onStateChange, events,
|
||||
} = inject(useCollectionStateKey)();
|
||||
currentState, modifyCurrentState, onStateChange,
|
||||
} = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const isReverted = ref(false);
|
||||
|
||||
@@ -39,24 +40,23 @@ export default defineComponent({
|
||||
|
||||
watch(
|
||||
() => props.node,
|
||||
async (node) => { await onNodeChanged(node); },
|
||||
(node) => onNodeChanged(node),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onStateChange((newState) => {
|
||||
updateRevertStatusFromState(newState.selection.selectedScripts);
|
||||
events.unsubscribeAll();
|
||||
events.register(
|
||||
events.unsubscribeAllAndRegister([
|
||||
newState.selection.changed.on((scripts) => updateRevertStatusFromState(scripts)),
|
||||
);
|
||||
]);
|
||||
}, { immediate: true });
|
||||
|
||||
async function onNodeChanged(node: INodeContent) {
|
||||
function onNodeChanged(node: INodeContent) {
|
||||
handler = getReverter(node, currentState.value.collection);
|
||||
updateRevertStatusFromState(currentState.value.selection.selectedScripts);
|
||||
}
|
||||
|
||||
async function updateRevertStatusFromState(scripts: ReadonlyArray<SelectedScript>) {
|
||||
function updateRevertStatusFromState(scripts: ReadonlyArray<SelectedScript>) {
|
||||
isReverted.value = handler?.getState(scripts) ?? false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
defineComponent, PropType, ref, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey, useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import ScriptsTree from '@/presentation/components/Scripts/View/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||
import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
|
||||
@@ -60,8 +60,9 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { modifyCurrentState, onStateChange, events } = inject(useCollectionStateKey)();
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { modifyCurrentState, onStateChange } = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const repositoryUrl = computed<string>(() => info.repositoryWebUrl);
|
||||
const searchQuery = ref<string | undefined>();
|
||||
@@ -77,9 +78,10 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
onStateChange((newState) => {
|
||||
events.unsubscribeAll();
|
||||
updateFromInitialFilter(newState.filter.currentFilter);
|
||||
subscribeToFilterChanges(newState.filter);
|
||||
events.unsubscribeAllAndRegister([
|
||||
subscribeToFilterChanges(newState.filter),
|
||||
]);
|
||||
}, { immediate: true });
|
||||
|
||||
function clearSearchQuery() {
|
||||
@@ -95,19 +97,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||
events.register(
|
||||
filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: (newFilter) => {
|
||||
searchQuery.value = newFilter.query;
|
||||
searchHasMatches.value = newFilter.hasAnyMatches();
|
||||
},
|
||||
onClear: () => {
|
||||
searchQuery.value = undefined;
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
return filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: (newFilter) => {
|
||||
searchQuery.value = newFilter.query;
|
||||
searchHasMatches.value = newFilter.hasAnyMatches();
|
||||
},
|
||||
onClear: () => {
|
||||
searchQuery.value = undefined;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { onUnmounted } from 'vue';
|
||||
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
||||
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||
|
||||
export function useAutoUnsubscribedEvents(
|
||||
events: IEventSubscriptionCollection = new EventSubscriptionCollection(),
|
||||
) {
|
||||
if (events.subscriptionCount > 0) {
|
||||
throw new Error('there are existing subscriptions, this may lead to side-effects');
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
events.unsubscribeAll();
|
||||
});
|
||||
|
||||
return {
|
||||
events,
|
||||
};
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
import { ref, computed, readonly } from 'vue';
|
||||
import {
|
||||
ref, computed, readonly,
|
||||
} from 'vue';
|
||||
import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
|
||||
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
|
||||
|
||||
export function useCollectionState(context: IApplicationContext) {
|
||||
export function useCollectionState(
|
||||
context: IApplicationContext,
|
||||
events: IEventSubscriptionCollection,
|
||||
) {
|
||||
if (!context) {
|
||||
throw new Error('missing context');
|
||||
}
|
||||
|
||||
const events = new EventSubscriptionCollection();
|
||||
const ownEvents = new EventSubscriptionCollection();
|
||||
if (!events) {
|
||||
throw new Error('missing events');
|
||||
}
|
||||
|
||||
const currentState = ref<ICategoryCollectionState>(context.state);
|
||||
ownEvents.register(
|
||||
events.register([
|
||||
context.contextChanged.on((event) => {
|
||||
currentState.value = event.newState;
|
||||
}),
|
||||
);
|
||||
]);
|
||||
|
||||
const defaultSettings: IStateCallbackSettings = {
|
||||
immediate: false,
|
||||
@@ -29,11 +33,11 @@ export function useCollectionState(context: IApplicationContext) {
|
||||
if (!handler) {
|
||||
throw new Error('missing state handler');
|
||||
}
|
||||
ownEvents.register(
|
||||
events.register([
|
||||
context.contextChanged.on((event) => {
|
||||
handler(event.newState, event.oldState);
|
||||
}),
|
||||
);
|
||||
]);
|
||||
const defaultedSettings: IStateCallbackSettings = {
|
||||
...defaultSettings,
|
||||
...settings,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import DownloadUrlListItem from './DownloadUrlListItem.vue';
|
||||
|
||||
const supportedOperativeSystems: readonly OperatingSystem[] = [
|
||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
||||
DownloadUrlListItem,
|
||||
},
|
||||
setup() {
|
||||
const { os: currentOs } = inject(useRuntimeEnvironmentKey);
|
||||
const { os: currentOs } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
const supportedDesktops = [
|
||||
...supportedOperativeSystems,
|
||||
].sort((os) => (os === currentOs ? 0 : 1));
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
defineComponent, PropType, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -25,8 +25,8 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { os: currentOs } = inject(useRuntimeEnvironmentKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
const { os: currentOs } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
|
||||
const isCurrentOs = computed<boolean>(() => {
|
||||
return currentOs === props.operatingSystem;
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, inject } from 'vue';
|
||||
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
const { isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
|
||||
const repositoryUrl = computed<string>(() => info.repositoryUrl);
|
||||
const feedbackUrl = computed<string>(() => info.feedbackUrl);
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
defineComponent, ref, computed, inject,
|
||||
} from 'vue';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import { useApplicationKey, useRuntimeEnvironmentKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import DownloadUrlList from './DownloadUrlList.vue';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
|
||||
@@ -59,8 +59,8 @@ export default defineComponent({
|
||||
DownloadUrlList,
|
||||
},
|
||||
setup() {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { isDesktop } = inject(useRuntimeEnvironmentKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
const { isDesktop } = inject(InjectionKeys.useRuntimeEnvironment);
|
||||
|
||||
const isPrivacyDialogVisible = ref(false);
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, inject } from 'vue';
|
||||
import { useApplicationKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { info } = inject(useApplicationKey);
|
||||
const { info } = inject(InjectionKeys.useApplication);
|
||||
|
||||
const title = computed(() => info.name);
|
||||
const subtitle = computed(() => info.slogan);
|
||||
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
defineComponent, ref, watch, computed,
|
||||
inject,
|
||||
} from 'vue';
|
||||
import { useCollectionStateKey } from '@/presentation/injectionSymbols';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
|
||||
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({
|
||||
directives: {
|
||||
@@ -28,8 +29,9 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
modifyCurrentState, onStateChange, events, currentState,
|
||||
} = inject(useCollectionStateKey)();
|
||||
modifyCurrentState, onStateChange, currentState,
|
||||
} = inject(InjectionKeys.useCollectionState)();
|
||||
const { events } = inject(InjectionKeys.useAutoUnsubscribedEvents)();
|
||||
|
||||
const searchPlaceholder = computed<string>(() => {
|
||||
const { totalScripts } = currentState.value.collection;
|
||||
@@ -51,28 +53,29 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
onStateChange((newState) => {
|
||||
events.unsubscribeAll();
|
||||
updateFromInitialFilter(newState.filter.currentFilter);
|
||||
subscribeToFilterChanges(newState.filter);
|
||||
events.unsubscribeAllAndRegister([
|
||||
subscribeToFilterChanges(newState.filter),
|
||||
]);
|
||||
}, { immediate: true });
|
||||
|
||||
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||
searchQuery.value = filter?.query || '';
|
||||
}
|
||||
|
||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||
events.register(
|
||||
filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: (result) => {
|
||||
searchQuery.value = result.query;
|
||||
},
|
||||
onClear: () => {
|
||||
searchQuery.value = '';
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
function subscribeToFilterChanges(
|
||||
filter: IReadOnlyUserFilter,
|
||||
): IEventSubscription {
|
||||
return filter.filterChanged.on((event) => {
|
||||
event.visit({
|
||||
onApply: (result) => {
|
||||
searchQuery.value = result.query;
|
||||
},
|
||||
onClear: () => {
|
||||
searchQuery.value = '';
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user