forked from mirrors/homebox
fix(frontend): fix mis-matched state errors (#9)
implenmented a store for each global item and tied it to an event bus and used the listener on the requests object to intercept responses from the API and run the appripriate get call when updates were detected.
This commit is contained in:
parent
724495cfca
commit
90813abf76
12 changed files with 247 additions and 34 deletions
|
@ -60,7 +60,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
console.log("updated");
|
|
||||||
if (props.inline) {
|
if (props.inline) {
|
||||||
setHeight();
|
setHeight();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
|
import { ItemCreate, LocationOut } from "~~/lib/api/types/data-contracts";
|
||||||
|
import { useLabelStore } from "~~/stores/labels";
|
||||||
|
import { useLocationStore } from "~~/stores/locations";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
@ -66,15 +68,11 @@
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const toast = useNotifier();
|
const toast = useNotifier();
|
||||||
|
|
||||||
const { data: locations } = useAsyncData(async () => {
|
const locationsStore = useLocationStore();
|
||||||
const { data } = await api.locations.getAll();
|
const locations = computed(() => locationsStore.locations);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: labels } = useAsyncData(async () => {
|
const labelStore = useLabelStore();
|
||||||
const { data } = await api.labels.getAll();
|
const labels = computed(() => labelStore.labels);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
if (!form.location) {
|
if (!form.location) {
|
||||||
|
|
|
@ -62,9 +62,7 @@
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
toast.success("Location created");
|
toast.success("Location created");
|
||||||
navigateTo(`/location/${data.id}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,6 +3,22 @@ import { UserApi } from "~~/lib/api/user";
|
||||||
import { Requests } from "~~/lib/requests";
|
import { Requests } from "~~/lib/requests";
|
||||||
import { useAuthStore } from "~~/stores/auth";
|
import { useAuthStore } from "~~/stores/auth";
|
||||||
|
|
||||||
|
export type Observer = {
|
||||||
|
handler: (r: Response) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveObserver = () => void;
|
||||||
|
|
||||||
|
const observers: Record<string, Observer> = {};
|
||||||
|
|
||||||
|
export function defineObserver(key: string, observer: Observer): RemoveObserver {
|
||||||
|
observers[key] = observer;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete observers[key];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function logger(r: Response) {
|
function logger(r: Response) {
|
||||||
console.log(`${r.status} ${r.url} ${r.statusText}`);
|
console.log(`${r.status} ${r.url} ${r.statusText}`);
|
||||||
}
|
}
|
||||||
|
@ -23,5 +39,9 @@ export function useUserApi(): UserApi {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const [_, observer] of Object.entries(observers)) {
|
||||||
|
requests.addResponseInterceptor(observer.handler);
|
||||||
|
}
|
||||||
|
|
||||||
return new UserApi(requests);
|
return new UserApi(requests);
|
||||||
}
|
}
|
||||||
|
|
38
frontend/composables/use-events.ts
Normal file
38
frontend/composables/use-events.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
export enum EventTypes {
|
||||||
|
// ClearStores event is used to inform the stores that _all_ the data they are using
|
||||||
|
// is now out of date and they should refresh - This is used when the user makes large
|
||||||
|
// changes to the data such as bulk actions or importing a CSV file
|
||||||
|
ClearStores,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventFn = () => void;
|
||||||
|
|
||||||
|
export interface IEventBus {
|
||||||
|
on(event: EventTypes, fn: EventFn, key: string): void;
|
||||||
|
off(event: EventTypes, key: string): void;
|
||||||
|
emit(event: EventTypes): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventBus implements IEventBus {
|
||||||
|
private listeners: Record<EventTypes, Record<string, EventFn>> = {
|
||||||
|
[EventTypes.ClearStores]: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
on(event: EventTypes, fn: EventFn, key: string): void {
|
||||||
|
this.listeners[event][key] = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: EventTypes, key: string): void {
|
||||||
|
delete this.listeners[event][key];
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(event: EventTypes): void {
|
||||||
|
Object.values(this.listeners[event]).forEach(fn => fn());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bus = new EventBus();
|
||||||
|
|
||||||
|
export function useEventBus(): IEventBus {
|
||||||
|
return bus;
|
||||||
|
}
|
|
@ -7,3 +7,63 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useItemStore } from "~~/stores/items";
|
||||||
|
import { useLabelStore } from "~~/stores/labels";
|
||||||
|
import { useLocationStore } from "~~/stores/locations";
|
||||||
|
/**
|
||||||
|
* Store Provider Initialization
|
||||||
|
*/
|
||||||
|
|
||||||
|
const labelStore = useLabelStore();
|
||||||
|
const reLabel = /\/api\/v1\/labels\/.*/gm;
|
||||||
|
const rmLabelStoreObserver = defineObserver("labelStore", {
|
||||||
|
handler: r => {
|
||||||
|
if (r.status === 201 || r.url.match(reLabel)) {
|
||||||
|
labelStore.refresh();
|
||||||
|
}
|
||||||
|
console.debug("labelStore handler called by observer");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
const reLocation = /\/api\/v1\/locations\/.*/gm;
|
||||||
|
const rmLocationStoreObserver = defineObserver("locationStore", {
|
||||||
|
handler: r => {
|
||||||
|
if (r.status === 201 || r.url.match(reLocation)) {
|
||||||
|
locationStore.refresh();
|
||||||
|
}
|
||||||
|
console.debug("locationStore handler called by observer");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemStore = useItemStore();
|
||||||
|
const reItem = /\/api\/v1\/items\/.*/gm;
|
||||||
|
const rmItemStoreObserver = defineObserver("itemStore", {
|
||||||
|
handler: r => {
|
||||||
|
if (r.status === 201 || r.url.match(reItem)) {
|
||||||
|
itemStore.refresh();
|
||||||
|
}
|
||||||
|
console.debug("itemStore handler called by observer");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventBus = useEventBus();
|
||||||
|
eventBus.on(
|
||||||
|
EventTypes.ClearStores,
|
||||||
|
() => {
|
||||||
|
labelStore.refresh();
|
||||||
|
itemStore.refresh();
|
||||||
|
locationStore.refresh();
|
||||||
|
},
|
||||||
|
"stores"
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
rmLabelStoreObserver();
|
||||||
|
rmLocationStoreObserver();
|
||||||
|
rmItemStoreObserver();
|
||||||
|
eventBus.off(EventTypes.ClearStores, "stores");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useItemStore } from "~~/stores/items";
|
||||||
|
import { useLabelStore } from "~~/stores/labels";
|
||||||
|
import { useLocationStore } from "~~/stores/locations";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "home",
|
layout: "home",
|
||||||
});
|
});
|
||||||
|
@ -8,20 +12,14 @@
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
const { data: locations } = useAsyncData("locations", async () => {
|
const itemsStore = useItemStore();
|
||||||
const { data } = await api.locations.getAll();
|
const items = computed(() => itemsStore.items);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: labels } = useAsyncData("labels", async () => {
|
const locationStore = useLocationStore();
|
||||||
const { data } = await api.labels.getAll();
|
const locations = computed(() => locationStore.locations);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: items } = useAsyncData("items", async () => {
|
const labelsStore = useLabelStore();
|
||||||
const { data } = await api.items.getAll();
|
const labels = computed(() => labelsStore.labels);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalItems = computed(() => items.value?.length || 0);
|
const totalItems = computed(() => items.value?.length || 0);
|
||||||
const totalLocations = computed(() => locations.value?.length || 0);
|
const totalLocations = computed(() => locations.value?.length || 0);
|
||||||
|
@ -67,6 +65,8 @@
|
||||||
importRef.value.click();
|
importRef.value.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventBus = useEventBus();
|
||||||
|
|
||||||
async function submitCsvFile() {
|
async function submitCsvFile() {
|
||||||
importLoading.value = true;
|
importLoading.value = true;
|
||||||
|
|
||||||
|
@ -81,6 +81,8 @@
|
||||||
importLoading.value = false;
|
importLoading.value = false;
|
||||||
importCsv.value = null;
|
importCsv.value = null;
|
||||||
importRef.value.value = null;
|
importRef.value.value = null;
|
||||||
|
|
||||||
|
eventBus.emit(EventTypes.ClearStores);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ItemUpdate } from "~~/lib/api/types/data-contracts";
|
import { ItemUpdate } from "~~/lib/api/types/data-contracts";
|
||||||
|
import { useLabelStore } from "~~/stores/labels";
|
||||||
|
import { useLocationStore } from "~~/stores/locations";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: "home",
|
layout: "home",
|
||||||
|
@ -12,17 +14,13 @@
|
||||||
|
|
||||||
const itemId = computed<string>(() => route.params.id as string);
|
const itemId = computed<string>(() => route.params.id as string);
|
||||||
|
|
||||||
const { data: locations } = useAsyncData(async () => {
|
const locationStore = useLocationStore();
|
||||||
const { data } = await api.locations.getAll();
|
const locations = computed(() => locationStore.locations);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: labels } = useAsyncData(async () => {
|
const labelStore = useLabelStore();
|
||||||
const { data } = await api.labels.getAll();
|
const labels = computed(() => labelStore.labels);
|
||||||
return data.items;
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: item } = useAsyncData(async () => {
|
const { data: item, refresh } = useAsyncData(async () => {
|
||||||
const { data, error } = await api.items.get(itemId.value);
|
const { data, error } = await api.items.get(itemId.value);
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error("Failed to load item");
|
toast.error("Failed to load item");
|
||||||
|
@ -32,6 +30,9 @@
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
|
||||||
async function saveItem() {
|
async function saveItem() {
|
||||||
const payload: ItemUpdate = {
|
const payload: ItemUpdate = {
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger Refresh on navigate
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
|
|
33
frontend/stores/items.ts
Normal file
33
frontend/stores/items.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ItemOut } from "~~/lib/api/types/data-contracts";
|
||||||
|
|
||||||
|
export const useItemStore = defineStore("items", {
|
||||||
|
state: () => ({
|
||||||
|
allItems: null as ItemOut[] | null,
|
||||||
|
client: useUserApi(),
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* items represents the items that are currently in the store. The store is
|
||||||
|
* synched with the server by intercepting the API calls and updating on the
|
||||||
|
* response.
|
||||||
|
*/
|
||||||
|
items(state): ItemOut[] {
|
||||||
|
if (state.allItems === null) {
|
||||||
|
Promise.resolve(this.refresh());
|
||||||
|
}
|
||||||
|
return state.allItems;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async refresh(): Promise<ItemOut[]> {
|
||||||
|
const result = await this.client.items.getAll();
|
||||||
|
if (result.error) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allItems = result.data.items;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
33
frontend/stores/labels.ts
Normal file
33
frontend/stores/labels.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { LabelOut } from "~~/lib/api/types/data-contracts";
|
||||||
|
|
||||||
|
export const useLabelStore = defineStore("labels", {
|
||||||
|
state: () => ({
|
||||||
|
allLabels: null as LabelOut[] | null,
|
||||||
|
client: useUserApi(),
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* labels represents the labels that are currently in the store. The store is
|
||||||
|
* synched with the server by intercepting the API calls and updating on the
|
||||||
|
* response.
|
||||||
|
*/
|
||||||
|
labels(state): LabelOut[] {
|
||||||
|
if (state.allLabels === null) {
|
||||||
|
Promise.resolve(this.refresh());
|
||||||
|
}
|
||||||
|
return state.allLabels;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async refresh(): Promise<LabelOut[]> {
|
||||||
|
const result = await this.client.labels.getAll();
|
||||||
|
if (result.error) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allLabels = result.data.items;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
33
frontend/stores/locations.ts
Normal file
33
frontend/stores/locations.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { LocationCount } from "~~/lib/api/types/data-contracts";
|
||||||
|
|
||||||
|
export const useLocationStore = defineStore("locations", {
|
||||||
|
state: () => ({
|
||||||
|
allLocations: null as LocationCount[] | null,
|
||||||
|
client: useUserApi(),
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* locations represents the locations that are currently in the store. The store is
|
||||||
|
* synched with the server by intercepting the API calls and updating on the
|
||||||
|
* response
|
||||||
|
*/
|
||||||
|
locations(state): LocationCount[] {
|
||||||
|
if (state.allLocations === null) {
|
||||||
|
Promise.resolve(this.refresh());
|
||||||
|
}
|
||||||
|
return state.allLocations;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async refresh(): Promise<LocationCount[]> {
|
||||||
|
const result = await this.client.locations.getAll();
|
||||||
|
if (result.error) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allLocations = result.data.items;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue