Skip to content

Commit

Permalink
Merge branch 'master' of github.com:add-eus/library
Browse files Browse the repository at this point in the history
  • Loading branch information
maximeallanic committed Jan 15, 2024
2 parents bf47fd9 + 382f735 commit 99a8ba8
Show file tree
Hide file tree
Showing 14 changed files with 912 additions and 73 deletions.
2 changes: 1 addition & 1 deletion components/base/layouts/VViewWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -261,5 +261,5 @@ const props = defineProps<VViewWrapperProps>();
width: 100%;
margin-left: 0;
}
}*/
} */
</style>
33 changes: 8 additions & 25 deletions stores/darkmode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @see /src/components/partials/toolbars/Toolbar.vue
*/

import { computed, watchEffect, ref } from "vue";
import { computed, watchEffect } from "vue";
import { usePreferredDark, useStorage } from "@vueuse/core";
import { acceptHMRUpdate, defineStore } from "pinia";
import tinyColor from "tinycolor2";
Expand All @@ -26,7 +26,8 @@ export const initDarkmode = () => {
watchEffect(() => {
const body = document.documentElement;

if (darkmode.isDark) {
const isDark = darkmode.isDark as any;
if (isDark === true) {
body.classList.add(DARK_MODE_BODY_CLASS);
} else {
body.classList.remove(DARK_MODE_BODY_CLASS);
Expand All @@ -36,10 +37,10 @@ export const initDarkmode = () => {
};

export const useDarkmode = defineStore("darkmode", () => {
let preferredDark;
const preferredDark = usePreferredDark();
const colorSchema = useStorage<DarkModeSchema>("color-schema", "auto");

/*window.matchMedia("(prefers-color-scheme: dark)");
/* window.matchMedia("(prefers-color-scheme: dark)");
window.addEventListener("change", (e) => {
colorSchema.value = e.matches ? "dark" : "light";
});*/
Expand All @@ -60,23 +61,14 @@ export const useDarkmode = defineStore("darkmode", () => {
const metaThemeColor = document.querySelector("meta[name=theme-color]");

const setHasDark =
colorSchema.value == "dark" ||
(colorSchema.value == "auto" && preferredDark.value);
colorSchema.value === "dark" ||
(colorSchema.value === "auto" && preferredDark.value === true);

const colorVar = getComputedStyle(document.documentElement).getPropertyValue(
setHasDark ? "--dark-sidebar-light-6" : "--white"
);
const colorHex = tinyColor(colorVar).toHex();
metaThemeColor.setAttribute("content", colorHex);

if (typeof cordova !== "undefined") {
StatusBar.backgroundColorByHexString(colorHex);

if (setHasDark) StatusBar.styleLightContent();
else StatusBar.styleDefault();

NavigationBar.backgroundColorByHexString(colorHex, !setHasDark);
}
metaThemeColor?.setAttribute("content", colorHex);
};

const onChange = (event: Event) => {
Expand All @@ -88,15 +80,6 @@ export const useDarkmode = defineStore("darkmode", () => {
isDark.value = !isDark.value;
};

if (typeof cordova !== "undefined") {
preferredDark = ref(false);
cordova.plugins.osTheme.getTheme().then((theme) => {
preferredDark.value = theme.isDark;
});
} else {
preferredDark = usePreferredDark();
}

return {
isDark,
onChange,
Expand Down
5 changes: 0 additions & 5 deletions stores/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
// Import the functions you need from the SDKs you need
import type { Auth } from "firebase/auth";
import { getAuth, connectAuthEmulator } from "firebase/auth";
import type { Firestore } from "firebase/firestore";
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
import type { Functions } from "firebase/functions";
import { getFunctions, connectFunctionsEmulator } from "firebase/functions";
import type { FirebaseStorage } from "firebase/storage";
import { getStorage, connectStorageEmulator } from "firebase/storage";
import { getAnalytics, initializeAnalytics } from "firebase/analytics";
import { getPerformance, initializePerformance } from "firebase/performance";
import type { Database } from "firebase/database";
import { getDatabase, connectDatabaseEmulator } from "firebase/database";
import { initializeApp } from "firebase/app";
import { getRemoteConfig, fetchAndActivate } from "firebase/remote-config";
Expand Down
296 changes: 294 additions & 2 deletions stores/firestore/collection.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,295 @@
import { Query } from "./query";
import type {
CollectionOptions as UseCollectionOption,
Collection as UseCollectionType,
} from ".";
import { newDoc, useCollection } from ".";
import type { Entity } from "./entity";
import { onInitialize } from "./entity";
import type { EntityMetaData } from "./entityMetadata";
import { useFirebase } from "addeus-common-library/stores/firebase";
import type { DocumentData, DocumentReference } from "firebase/firestore";
import { collection, deleteDoc, doc, getDoc, setDoc } from "firebase/firestore";
import { shallowReactive } from "vue";
import { watchArray } from "@vueuse/core";
import { securityCollectionCallbacks } from "./security/securityDecorators";

export class Collection extends Query {}
export type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

export type NonFunctionProperties<T> = {
[K in Exclude<keyof T, FunctionPropertyNames<T>>]: T[K];
};

export interface CollectionOptions<T> {
namespace: string;
backlistFields?: Partial<
Omit<Record<keyof NonFunctionProperties<T>, boolean>, "blacklistedProperties">
>;
}

export interface EntityInfo {
model: typeof Entity;
subPaths: { path: string; blacklistedProperties: string[] }[];
}

/**
* Map model to namespace of all entities
*/
export const entitiesInfos = new Map<string, EntityInfo>();

const onCollectionsInitialize = new Map<string, (() => void)[]>();

export function Collection<T>(options: CollectionOptions<T>) {
return function (target: any, propertyKey?: string) {
// On class
if (propertyKey === undefined) {
target.collectionName = options.namespace;

// Associate namespace to model
entitiesInfos.set(target.collectionName, {
model: target,
subPaths: [{ path: target.collectionName, blacklistedProperties: [] }],
});
onCollectionsInitialize.get(target.collectionName)?.forEach((init) => init());
securityCollectionCallbacks.get(target.name)?.forEach((init) => init());
}
// On property
else {
if (options.namespace === undefined) {
throw new Error("namespace is undefined");
}
const namespace = options.namespace;
const onCollectionInitialize = () => {
const info = entitiesInfos.get(namespace);
if (info === undefined) {
throw new Error(`${namespace} info is undefined`);
}

const blacklistedProperties = Object.entries(options.backlistFields ?? {})
.filter(([, value]) => value)
.map(([key]) => key);

// save propertyKey in subCollections of model
const subPathInfo = info.subPaths.find(
({ path }) => path === propertyKey
);
if (subPathInfo !== undefined) {
subPathInfo.blacklistedProperties.forEach((blacklistedProperty) => {
if (!blacklistedProperties.includes(blacklistedProperty)) {
throw new Error(
`property ${propertyKey} already exists, a subcollection name must have the same blacklistedProperties`
);
}
});
} else {
info.subPaths.push({
path: propertyKey,
blacklistedProperties,
});
}

onInitialize(target, function (this: any, metadata: EntityMetaData) {
// tag property as collection property, used in Entity to save and parse this property
metadata.collectionProperties[propertyKey] = {
namespace,
blacklistedProperties,
};
});
};

// wait Collection decorator on model
const info = entitiesInfos.get(namespace);
if (info === undefined) {
const inits = onCollectionsInitialize.get(namespace);
if (inits === undefined) {
onCollectionsInitialize.set(namespace, [onCollectionInitialize]);
} else {
inits.push(onCollectionInitialize);
}
} else {
onCollectionInitialize();
}
}
};
}

export class SubCollection<T extends Entity> {
private firestoreArray?: UseCollectionType<T>;
private currentList = shallowReactive(new Array<T>());
private model?: typeof Entity;
private path?: string;
private initialized = false;
private stopWatch?: () => void;
private isFetched = false;
private new = false;
public blacklistedProperties: string[] = [];

init(
model: typeof Entity,
path: string | undefined,
blacklistedProperties: string[]
) {
this.model = model;
this.path = path;
this.blacklistedProperties = blacklistedProperties;
this.initialized = true;
this.new = path === undefined;
}

setOptions(options?: Omit<UseCollectionOption, "path" | "blacklistedProperties">) {
if (!this.initialized) throw new Error(`property subcollection not initialized`);
if (this.model === undefined)
throw new Error(`model in property ${this.path} is undefined`);
if (this.path === undefined)
throw new Error(`path in property ${this.path} is undefined`);

this.stopWatch?.();

const useCollectionOptions: UseCollectionOption = {
...options,
path: this.path,
blacklistedProperties: this.blacklistedProperties,
};
this.firestoreArray = useCollection(this.model, useCollectionOptions) as any;

this.currentList.splice(0, this.currentList.length);

this.stopWatch = watchArray(
this.firestoreArray!,
(value, oldValue, added: T[], removed: T[]) => {
added.forEach((a) => {
const alreadyInArray = this.currentList.some(
(entity) => entity.$getID() === a.$getID()
);
if (!alreadyInArray) this.currentList.push(a);
});
removed.forEach((r) => {
const index = this.currentList.findIndex(
(entity) => entity.$getID() === r.$getID()
);
if (index !== -1) this.currentList.splice(index, 1);
});
}
);
this.isFetched = true;
}

async exists(entity: Entity): Promise<boolean> {
const id = entity.$getID();
if (id === undefined) throw new Error("id is undefined");
return await this.existsById(id);
}

async existsById(id: string): Promise<boolean> {
if (!this.initialized) throw new Error(`property subcollection not initialized`);
if (this.new) throw new Error(`property subcollection is new`);
const firebase = useFirebase();
const snap = await getDoc(doc(firebase.firestore, `${this.path}/${id}`));
return snap.exists();
}

get list() {
if (!this.isFetched && !this.new) this.setOptions();
return this.currentList;
}

get entityModel() {
return this.model;
}

get isInitialized() {
return this.initialized;
}

get isNew() {
return this.new;
}

fetched() {
if (!this.isFetched) throw new Error(`property subcollection not initialized`);
if (this.firestoreArray === undefined)
throw new Error(`firestoreArray is undefined`);
return this.firestoreArray.fetched();
}

/**
* Get array modification between app datas and firestore datas
* @param appArray Current data in app (with new or deleted entities)
* @param firestoreArray Current data in firestore
* @returns {toDelete, toAdd} entities to delete and entities to add
*/
getArrayModification() {
const dbEntities: Entity[] = [];
if (this.firestoreArray) dbEntities.push(...this.firestoreArray);
const appEntities = [...this.currentList];

const toDelete = dbEntities.filter(
(f) => !appEntities.some((a) => a.$getID() === f.$getID())
);
const toAdd = appEntities.filter(
(a) => !dbEntities.some((f) => a.$getID() === f.$getID())
);
return { toDelete, toAdd };
}

newDoc(): T {
if (!this.isInitialized)
throw new Error(`property subcollection not initialized`);
if (this.model === undefined) throw new Error(`model is undefined`);
const entity = newDoc(this.model) as T;
entity.$getMetadata().saveNewDocPath = this.path;
this.currentList.push(entity);
return entity;
}
}

/**
* Add and/or remove elements from firestore
* @param toRemove elements to remove
* @param toAdd elements to add
* @param path path of the collection
*/
export const updatePropertyCollection = async (
toRemove: Array<Entity>,
toAdd: Array<Entity>,
path: string,
blacklistedProperties: string[] = []
) => {
const firebase = useFirebase();

const collectionRef = collection(firebase.firestore, path);
const removePromises = toRemove.map(async (entity) => {
const id = entity.$getMetadata().reference!.id;
const docRef = doc(collectionRef, id);
await deleteDoc(docRef);
});
const addPromises = toAdd.map(async (entity) => {
let id = entity.$getMetadata().reference?.id;

// entity is already in collection
if (entity.$getMetadata().reference?.path === `${collectionRef.path}/${id}`) {
return;
}

let docRef: DocumentReference<DocumentData> | undefined;
if (id === undefined) {
// entity is new, save it before add to collection
await entity.$save();
id = entity.$getMetadata().reference!.id;
docRef = entity.$getMetadata().reference!;
} else {
docRef = doc(collectionRef, id);
await setDoc(docRef, entity.$getPlain(), { merge: true });
}

// save sub collections of added entity recursively
const constructor = entity.constructor as typeof Entity;
const model = new constructor();
const metadata = model.$getMetadata();
metadata.setReference(docRef);
metadata.blacklistedProperties = blacklistedProperties;
await metadata.savePropertyCollections(entity);
});
await Promise.all([...removePromises, ...addPromises]);
};
Loading

0 comments on commit 99a8ba8

Please sign in to comment.