diff --git a/examples/example_pro/src/SampleApp/SampleApp.tsx b/examples/example_pro/src/SampleApp/SampleApp.tsx index 0ce6053c7..f119bab2f 100644 --- a/examples/example_pro/src/SampleApp/SampleApp.tsx +++ b/examples/example_pro/src/SampleApp/SampleApp.tsx @@ -22,7 +22,7 @@ import { localeCollectionGroup, productsCollection } from "./collections/product import { blogCollection } from "./collections/blog_collection"; import { showcaseCollection } from "./collections/showcase_collection"; -import { textSearchControllerBuilder } from "./text_search"; +import { algoliaSearchControllerBuilder, pineconeSearchControllerBuilder } from "./text_search"; import { CustomLoginView } from "./CustomLoginView"; import { cryptoCollection } from "./collections/crypto_collection"; @@ -162,7 +162,7 @@ function SampleApp() { // 'microsoft.com', // 'apple.com' ]} - textSearchControllerBuilder={textSearchControllerBuilder} + textSearchControllerBuilder={pineconeSearchControllerBuilder} firestoreIndexesBuilder={firestoreIndexesBuilder} logo={logo} collections={(params) => collections} diff --git a/examples/example_pro/src/SampleApp/text_search.ts b/examples/example_pro/src/SampleApp/text_search.ts index 15ce48b10..73214bf7c 100644 --- a/examples/example_pro/src/SampleApp/text_search.ts +++ b/examples/example_pro/src/SampleApp/text_search.ts @@ -1,6 +1,11 @@ import algoliasearch, { SearchClient } from "algoliasearch"; -import { buildAlgoliaSearchController, performAlgoliaTextSearch } from "@firecms/firebase_pro"; +import { + buildAlgoliaSearchController, + buildPineconeSearchController, + performAlgoliaTextSearch, + performPineconeTextSearch +} from "@firecms/firebase_pro"; let client: SearchClient | undefined; // process is defined for react-scripts builds @@ -24,7 +29,7 @@ const usersIndex = client && client.initIndex("users"); const blogIndex = client && client.initIndex("blog"); const booksIndex = client && client.initIndex("books"); -export const textSearchControllerBuilder = buildAlgoliaSearchController({ +export const algoliaSearchControllerBuilder = buildAlgoliaSearchController({ isPathSupported: (path) => { return ["products", "users", "blog", "books"].includes(path); }, @@ -43,3 +48,23 @@ export const textSearchControllerBuilder = buildAlgoliaSearchController({ return undefined; } }); + +export const pineconeSearchControllerBuilder = buildPineconeSearchController({ + isPathSupported: (path) => { + return ["products"].includes(path); + }, + search: async ({ + path, + searchString, + currentUser + }) => { + if (path === "products") + return performPineconeTextSearch({ + firebaseToken: await currentUser.getIdToken(), + projectId: "firecms-backend", + collectionPath: "products", + query: searchString + }); + throw new Error("Path not supported"); + } +}); diff --git a/examples/example_v3/src/App.tsx b/examples/example_v3/src/App.tsx index 3aeb4f72a..47dbb06a8 100644 --- a/examples/example_v3/src/App.tsx +++ b/examples/example_v3/src/App.tsx @@ -1,9 +1,9 @@ import React from "react" -import { FireCMS3App } from "firecms"; +import { FireCMSApp } from "firecms"; import appConfig from "./index"; function App(): React.ReactElement { - return { + return ["products"].includes(path); + }, + search: async ({ + path, + searchString, + currentUser + }) => { + if (path === "products") + return performPineconeTextSearch({ + firebaseToken: await currentUser.getIdToken(), + projectId: "firecms-demo-27150", + collectionPath: "products", + query: searchString + }); + throw new Error("Path not supported"); + } +}); diff --git a/packages/firebase_firecms/src/hooks/useFirebaseAuthController.ts b/packages/firebase_firecms/src/hooks/useFirebaseAuthController.ts index 75e9489b0..010152190 100644 --- a/packages/firebase_firecms/src/hooks/useFirebaseAuthController.ts +++ b/packages/firebase_firecms/src/hooks/useFirebaseAuthController.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ApplicationVerifier, @@ -47,11 +47,13 @@ export const useFirebaseAuthController = ({ const [confirmationResult, setConfirmationResult] = useState(); const [userRoles, setUserRoles] = useState(null); + const authRef = useRef(firebaseApp ? getAuth(firebaseApp) : null); useEffect(() => { if (!firebaseApp) return; try { const auth = getAuth(firebaseApp); + authRef.current = auth; setAuthError(undefined); setLoggedUser(auth.currentUser) return onAuthStateChanged( @@ -82,7 +84,8 @@ export const useFirebaseAuthController = ({ options.scopes.forEach((scope) => provider.addScope(scope)); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(firebaseApp); + const auth = authRef.current; + if(!auth) throw Error("No auth"); signInWithPopup(auth, provider).catch(setAuthProviderError); }, [getProviderOptions]); @@ -93,7 +96,8 @@ export const useFirebaseAuthController = ({ }, [loggedUser]); const emailPasswordLogin = useCallback((email: string, password: string) => { - const auth = getAuth(firebaseApp); + const auth = authRef.current; + if(!auth) throw Error("No auth"); setAuthLoading(true); signInWithEmailAndPassword(auth, email, password) .catch(setAuthProviderError) @@ -101,7 +105,8 @@ export const useFirebaseAuthController = ({ }, []); const createUserWithEmailAndPassword = useCallback((email: string, password: string) => { - const auth = getAuth(firebaseApp); + const auth = authRef.current; + if(!auth) throw Error("No auth"); setAuthLoading(true); createUserWithEmailAndPasswordFirebase(auth, email, password) .catch(setAuthProviderError) @@ -109,7 +114,8 @@ export const useFirebaseAuthController = ({ }, []); const fetchSignInMethodsForEmail = useCallback((email: string): Promise => { - const auth = getAuth(firebaseApp); + const auth = authRef.current; + if(!auth) throw Error("No auth"); setAuthLoading(true); return fetchSignInMethodsForEmailFirebase(auth, email) .then((res) => { @@ -119,7 +125,8 @@ export const useFirebaseAuthController = ({ }, []); const onSignOut = useCallback(() => { - const auth = getAuth(firebaseApp); + const auth = authRef.current; + if(!auth) throw Error("No auth"); signOut(auth) .then(_ => { setLoggedUser(null); @@ -141,7 +148,8 @@ export const useFirebaseAuthController = ({ }, []); const anonymousLogin = useCallback(() => { - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); setAuthLoading(true); signInAnonymously(auth) .catch(setAuthProviderError) @@ -149,7 +157,8 @@ export const useFirebaseAuthController = ({ }, []); const phoneLogin = useCallback((phone: string, applicationVerifier: ApplicationVerifier) => { - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); setAuthLoading(true); return signInWithPhoneNumber(auth, phone, applicationVerifier) .catch(setAuthProviderError) @@ -166,7 +175,8 @@ export const useFirebaseAuthController = ({ options.scopes.forEach((scope) => provider.addScope(scope)); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); doOauthLogin(auth, provider); }, [doOauthLogin, getProviderOptions]); @@ -177,7 +187,8 @@ export const useFirebaseAuthController = ({ options.scopes.forEach((scope) => provider.addScope(scope)); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); doOauthLogin(auth, provider); }, [doOauthLogin, getProviderOptions]); @@ -188,7 +199,8 @@ export const useFirebaseAuthController = ({ options.scopes.forEach((scope) => provider.addScope(scope)); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); doOauthLogin(auth, provider); }, [doOauthLogin, getProviderOptions]); @@ -199,7 +211,8 @@ export const useFirebaseAuthController = ({ options.scopes.forEach((scope) => provider.addScope(scope)); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); doOauthLogin(auth, provider); }, [doOauthLogin, getProviderOptions]); @@ -208,11 +221,11 @@ export const useFirebaseAuthController = ({ const options = getProviderOptions("twitter.com"); if (options?.customParameters) provider.setCustomParameters(options.customParameters); - const auth = getAuth(); + const auth = authRef.current; + if(!auth) throw Error("No auth"); doOauthLogin(auth, provider); }, [doOauthLogin, getProviderOptions]); - const skipLogin = useCallback(() => { setLoginSkipped(true); setLoggedUser(null); diff --git a/packages/firebase_firecms/src/hooks/useFirestoreDelegate.ts b/packages/firebase_firecms/src/hooks/useFirestoreDelegate.ts index 6840afd70..3c63f0646 100644 --- a/packages/firebase_firecms/src/hooks/useFirestoreDelegate.ts +++ b/packages/firebase_firecms/src/hooks/useFirestoreDelegate.ts @@ -47,6 +47,7 @@ import { FirebaseApp } from "firebase/app"; import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types/text_search"; import { useCallback, useEffect, useRef } from "react"; import { localSearchControllerBuilder } from "../utils"; +import { getAuth } from "firebase/auth"; /** * @group Firebase @@ -192,10 +193,10 @@ export function useFirestoreDelegate({ }, [firebaseApp]); const performTextSearch = useCallback(>({ - path, - searchString, - onUpdate - }: { + path, + searchString, + onUpdate + }: { path: string, searchString: string; onUpdate: (entities: Entity[]) => void @@ -208,9 +209,15 @@ export function useFirestoreDelegate({ throw Error("Trying to make text search without specifying a FirestoreTextSearchController"); let subscriptions: (() => void)[] = []; + + const auth = getAuth(firebaseApp); + const currentUser = auth.currentUser; + if (!currentUser) throw Error("No current user"); + const search = textSearchController.search({ path, - searchString + searchString, + currentUser }); if (!search) { @@ -768,7 +775,8 @@ function buildTextSearchControllerWithLocalSearch({ }, search: (props: { searchString: string, - path: string + path: string, + currentUser: any }) => { return textSearchController.search(props) ?? localSearchController.search(props); } diff --git a/packages/firebase_firecms/src/hooks/useInitialiseFirebase.ts b/packages/firebase_firecms/src/hooks/useInitialiseFirebase.ts index 36087600f..a8ab38a36 100644 --- a/packages/firebase_firecms/src/hooks/useInitialiseFirebase.ts +++ b/packages/firebase_firecms/src/hooks/useInitialiseFirebase.ts @@ -24,7 +24,7 @@ const hostingError = "It seems like the provided Firebase config is not correct. * configuration. * * You most likely only need to use this if you are developing a custom app - * that is not using {@link FireCMS3App}. You can also not use this component + * that is not using {@link FireCMSApp}. You can also not use this component * and initialise Firebase yourself. * * @param onFirebaseInit diff --git a/packages/firebase_firecms/src/hooks/useInitializeAppCheck.ts b/packages/firebase_firecms/src/hooks/useInitializeAppCheck.ts index 0ba9a27ce..74275fd5e 100644 --- a/packages/firebase_firecms/src/hooks/useInitializeAppCheck.ts +++ b/packages/firebase_firecms/src/hooks/useInitializeAppCheck.ts @@ -29,7 +29,7 @@ export interface InitializeAppCheckResult { * It works as a hook that gives you back an object holding the Firebase App. * * You most likely only need to use this if you are developing a custom app - * that is not using {@link FireCMS3App}. You can also not use this component + * that is not using {@link FireCMSApp}. You can also not use this component * and initialise App Check yourself. * * @param firebaseApp diff --git a/packages/firebase_firecms/src/hooks/useValidateAuthenticator.tsx b/packages/firebase_firecms/src/hooks/useValidateAuthenticator.tsx index 0718f4b27..af1d64de3 100644 --- a/packages/firebase_firecms/src/hooks/useValidateAuthenticator.tsx +++ b/packages/firebase_firecms/src/hooks/useValidateAuthenticator.tsx @@ -6,7 +6,7 @@ import { Authenticator } from "../types/auth"; /** * This hook is used internally for validating an authenticator. - * You may want to use it if you are not using {@link FireCMS3App}, but + * You may want to use it if you are not using {@link FireCMSApp}, but * building your own custom {@link FireCMS} instance. * @param authController * @param authentication diff --git a/packages/firebase_firecms/src/types/text_search.ts b/packages/firebase_firecms/src/types/text_search.ts index 30f409707..4f684ba8a 100644 --- a/packages/firebase_firecms/src/types/text_search.ts +++ b/packages/firebase_firecms/src/types/text_search.ts @@ -1,3 +1,12 @@ + +import { User as FirebaseUser } from "firebase/auth"; +import { FirebaseApp } from "firebase/app"; +import { EntityCollection, ResolvedEntityCollection } from "@firecms/core"; + +export type FirestoreTextSearchControllerBuilder = (props: { + firebaseApp: FirebaseApp; +}) => FirestoreTextSearchController; + /** * Use this controller to return a list of ids from a search index, given a * `path` and a `searchString`. @@ -8,13 +17,6 @@ * @see performAlgoliaTextSearch * @group Firebase */ -import { FirebaseApp } from "firebase/app"; -import { EntityCollection, ResolvedEntityCollection } from "@firecms/core"; - -export type FirestoreTextSearchControllerBuilder = (props: { - firebaseApp: FirebaseApp; -}) => FirestoreTextSearchController; - export type FirestoreTextSearchController = { /** * This method is called when a search delegate is ready to be used. @@ -29,5 +31,5 @@ export type FirestoreTextSearchController = { * Do the search and return a list of ids. * @param props */ - search: (props: { searchString: string, path: string }) => (Promise | undefined), + search: (props: { searchString: string, path: string, currentUser: FirebaseUser }) => (Promise | undefined), }; diff --git a/packages/firebase_firecms/src/utils/index.ts b/packages/firebase_firecms/src/utils/index.ts index 662e16f0e..b39a7852f 100644 --- a/packages/firebase_firecms/src/utils/index.ts +++ b/packages/firebase_firecms/src/utils/index.ts @@ -1,4 +1,5 @@ export * from "./collections_firestore"; export * from "./database"; export * from "./algolia"; +export * from "./pinecone"; export * from "./local_text_search_controller"; diff --git a/packages/firebase_firecms/src/utils/pinecone.ts b/packages/firebase_firecms/src/utils/pinecone.ts index b7af7cb20..220750c0e 100644 --- a/packages/firebase_firecms/src/utils/pinecone.ts +++ b/packages/firebase_firecms/src/utils/pinecone.ts @@ -1,7 +1,9 @@ -import { SearchIndex } from "algoliasearch"; +import { User as FirebaseUser } from "firebase/auth"; import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types"; import { EntityCollection, ResolvedEntityCollection } from "@firecms/core"; +const DEFAULT_SERVER = "https://api-drplyi3b6q-ey.a.run.app"; + /** * Utility function to perform a text search in an algolia index, * returning the ids of the entities. @@ -9,28 +11,49 @@ import { EntityCollection, ResolvedEntityCollection } from "@firecms/core"; * @param query * @group Firebase */ -export function performAlgoliaTextSearch(index: SearchIndex, query: string): Promise { - - console.debug("Performing Algolia query", index, query); - return index - .search(query) - .then(({ hits }: any) => { - return hits.map((hit: any) => hit.objectID as string); - }) - .catch((err: any) => { - console.error(err); - return []; +export async function performPineconeTextSearch({ + host = DEFAULT_SERVER, + firebaseToken, + projectId, + collectionPath, + query + }: { + host?: string, + firebaseToken: string, + collectionPath: string, + projectId: string, + query: string +}): Promise { + + console.debug("Performing Pinecone query", collectionPath, query); + const response = await fetch((host ?? DEFAULT_SERVER) + `/projects/${projectId}/search/${collectionPath}`, + { + // mode: "no-cors", + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Basic ${firebaseToken}`, + // "x-de-version": version + }, + body: JSON.stringify({ + query + }) }); + + const promise = await response.json(); + return promise.data.ids; + } -export function buildAlgoliaSearchController({ +export function buildPineconeSearchController({ isPathSupported, search }: { isPathSupported: (path: string) => boolean, search: (props: { searchString: string, - path: string + path: string, + currentUser: FirebaseUser }) => Promise | undefined, }): FirestoreTextSearchControllerBuilder { return (props): FirestoreTextSearchController => { diff --git a/packages/firebase_firecms_pro/src/FireCMSProApp.tsx b/packages/firebase_firecms_pro/src/FireCMSProApp.tsx index 331680cf8..0d906caee 100644 --- a/packages/firebase_firecms_pro/src/FireCMSProApp.tsx +++ b/packages/firebase_firecms_pro/src/FireCMSProApp.tsx @@ -39,13 +39,6 @@ import { useInitializeAppCheck, useValidateAuthenticator } from "@firecms/firebase"; -import { - FireCMSBackend, - FireCMSBackEndProvider, - ProjectConfigProvider, - useBuildFireCMSBackend, - useBuildProjectConfig -} from "firecms"; import { FirebaseApp } from "firebase/app"; import { CenteredView } from "@firecms/ui"; @@ -169,20 +162,6 @@ export function FireCMSProApp({ storageSource }); - const { - firebaseApp: backendFirebaseApp, - firebaseConfigLoading: backendConfigLoading, - configError: backendConfigError, - firebaseConfigError: backendFirebaseConfigError - } = useInitialiseFirebase({ - fromUrl: backendApiHost + "/config" - }); - - const fireCMSBackend = useBuildFireCMSBackend({ - backendApiHost, - backendFirebaseApp, - }); - if (firebaseConfigLoading || !firebaseApp || appCheckLoading) { return <> @@ -191,7 +170,6 @@ export function FireCMSProApp({ return | undefined, - notAllowedError: any, name: string, + notAllowedError: any, + name: string, toolbarExtraWidget: React.ReactNode | undefined, autoOpenDrawer: boolean | undefined }) { @@ -257,10 +236,10 @@ function FireCMSProInternal({ firebaseApp, fireCMSBackend, configError, firebase if (!firebaseApp.options.projectId) { throw new Error("No firebase project id") } - const projectConfig = useBuildProjectConfig({ - projectId: firebaseApp.options.projectId, - backendFirebaseApp: fireCMSBackend.backendFirebaseApp, - }); + // const projectConfig = useBuildProjectConfig({ + // projectId: firebaseApp.options.projectId, + // backendFirebaseApp: fireCMSBackend.backendFirebaseApp, + // }); /** * Controller used to manage the dark or light color mode @@ -276,66 +255,62 @@ function FireCMSProInternal({ firebaseApp, fireCMSBackend, configError, firebase - - - `https://console.firebase.google.com/project/${firebaseApp.options.projectId}/firestore/data/${entity.path}/${entity.id}`} - locale={locale} - basePath={basePath} - baseCollectionPath={baseCollectionPath} - onAnalyticsEvent={onAnalyticsEvent} - plugins={plugins} - propertyConfigs={propertyConfigs}> - {({ - context, - loading - }) => { + `https://console.firebase.google.com/project/${firebaseApp.options.projectId}/firestore/data/${entity.path}/${entity.id}`} + locale={locale} + basePath={basePath} + baseCollectionPath={baseCollectionPath} + onAnalyticsEvent={onAnalyticsEvent} + plugins={plugins} + propertyConfigs={propertyConfigs}> + {({ + context, + loading + }) => { - let component; - if (loading || authLoading) { - component = ; - } else { - const usedLogo = modeController.mode === "dark" && logoDark ? logoDark : logo; - if (!canAccessMainView) { - const LoginViewUsed = components?.LoginView ?? FirebaseLoginView; - component = ( - - ); - } else { - component = ( - - - - - ); - } - } + let component; + if (loading || authLoading) { + component = ; + } else { + const usedLogo = modeController.mode === "dark" && logoDark ? logoDark : logo; + if (!canAccessMainView) { + const LoginViewUsed = components?.LoginView ?? FirebaseLoginView; + component = ( + + ); + } else { + component = ( + + + + + ); + } + } - return component; - }} - - - + return component; + }} + diff --git a/packages/firebase_firecms_pro/src/components/FirebaseLoginView.tsx b/packages/firebase_firecms_pro/src/components/FirebaseLoginView.tsx index cbfcd61d3..148f5e895 100644 --- a/packages/firebase_firecms_pro/src/components/FirebaseLoginView.tsx +++ b/packages/firebase_firecms_pro/src/components/FirebaseLoginView.tsx @@ -123,7 +123,7 @@ export function FirebaseLoginView({ }) const sendMFASms = useCallback(() => { - const auth = getAuth(); + const auth = getAuth(firebaseApp); const recaptchaVerifier = new RecaptchaVerifier(auth, "recaptcha", { size: "invisible" }); const resolver = getMultiFactorResolver(auth, authController.authProviderError); diff --git a/packages/firecms/src/FireCMS3App.tsx b/packages/firecms/src/FireCMSApp.tsx similarity index 96% rename from packages/firecms/src/FireCMS3App.tsx rename to packages/firecms/src/FireCMSApp.tsx index 361e6f1fb..34c84cb14 100644 --- a/packages/firecms/src/FireCMS3App.tsx +++ b/packages/firecms/src/FireCMSApp.tsx @@ -42,7 +42,7 @@ import { UserManagement, } from "./hooks"; -import { FireCMS3AppProps } from "./FireCMS3AppProps"; +import { FireCMSAppProps } from "./FireCMSAppProps"; import { FireCMSAppConfig, FireCMSBackend, FireCMSUser } from "./types"; import { ADMIN_VIEWS, @@ -87,14 +87,14 @@ const DOCS_LIMIT = 200; * @constructor * @group Firebase */ -export function FireCMS3App({ +export function FireCMSApp({ projectId, appConfig, backendApiHost = "https://api-drplyi3b6q-ey.a.run.app", // TODO onAnalyticsEvent, basePath, baseCollectionPath, - }: FireCMS3AppProps) { + }: FireCMSAppProps) { const modeController = useBuildModeController(); @@ -104,6 +104,7 @@ export function FireCMS3App({ configError, firebaseConfigError: backendFirebaseConfigError } = useInitialiseFirebase({ + name: "firecms-backend", fromUrl: backendApiHost + "/config" }); @@ -134,7 +135,7 @@ export function FireCMS3App({ includeGoogleDisclosure={false}/> } else { - component = = { +export type FireCMSClientProps = { fireCMSBackend: FireCMSBackend, projectId: string; appConfig?: FireCMSAppConfig; @@ -182,11 +183,11 @@ function FullLoadingView(props: { ; } -export const FireCMS3Client = function FireCMS3Client({ +export const FireCMSClient = function FireCMSClient({ projectId, fireCMSBackend, ...props - }: FireCMS3ClientProps) { + }: FireCMSClientProps) { const projectConfig = useBuildProjectConfig({ projectId, @@ -208,7 +209,7 @@ export const FireCMS3Client = function FireCMS3Client({ text={"Client loading"}/>; } - return ; }; -export function FireCMS3ClientWithController({ +export function FireCMSClientWithController({ projectConfig, userManagement, projectId, @@ -226,7 +227,7 @@ export function FireCMS3ClientWithController({ appConfig, customizationLoading, ...props - }: FireCMS3ClientProps & { + }: FireCMSClientProps & { userManagement: UserManagement; projectConfig: ProjectConfig; projectId: string; @@ -242,7 +243,7 @@ export function FireCMS3ClientWithController({ } = useInitialiseFirebase({ onFirebaseInit: appConfig?.onFirebaseInit, firebaseConfig: projectConfig.clientFirebaseConfig, - name: projectId + // name: projectId }); const authController: FirebaseAuthController = useFirebaseAuthController({ @@ -350,7 +351,7 @@ export function FireCMS3ClientWithController({ ; } - return ; } -function FireCMS3AppAuthenticated({ +function FireCMSAppAuthenticated({ fireCMSUser, firebaseApp, projectConfig, @@ -387,7 +388,7 @@ function FireCMS3AppAuthenticated({ onAnalyticsEvent, basePath, baseCollectionPath - }: Omit & { + }: Omit & { fireCMSUser: FireCMSUser; firebaseApp: FirebaseApp; projectConfig: ProjectConfig; @@ -398,7 +399,7 @@ function FireCMS3AppAuthenticated({ }) { if (!authController.user) { - throw Error("You can only use FireCMS3AppAuthenticated with an authenticated user"); + throw Error("You can only use FireCMSAppAuthenticated with an authenticated user"); } const adminRoutes = useMemo(buildAdminRoutes, []); diff --git a/packages/firecms/src/FireCMS3AppProps.tsx b/packages/firecms/src/FireCMSAppProps.tsx similarity index 96% rename from packages/firecms/src/FireCMS3AppProps.tsx rename to packages/firecms/src/FireCMSAppProps.tsx index e02a8af58..9322f3579 100644 --- a/packages/firecms/src/FireCMS3AppProps.tsx +++ b/packages/firecms/src/FireCMSAppProps.tsx @@ -4,7 +4,7 @@ import { FireCMSAppConfig } from "./types"; * Main entry point that defines the CMS configuration * @group Firebase */ -export type FireCMS3AppProps = { +export type FireCMSAppProps = { /** * Firebase project id this CMS is connected to. diff --git a/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx b/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx index 47c662ec4..6431ad2c3 100644 --- a/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx +++ b/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx @@ -107,13 +107,14 @@ export function useBuildFireCMSBackend({ backendApiHost, backendFirebaseApp, onU }, [loggedUser]); const googleLogin = useCallback((includeGoogleAdminScopes?: boolean) => { + if (!backendFirebaseApp) return; const provider = new GoogleAuthProvider(); provider.setCustomParameters({ access_type: "offline" }); if (includeGoogleAdminScopes) AUTH_SCOPES.forEach((scope) => provider.addScope(scope)); - const auth = getAuth(); + const auth = getAuth(backendFirebaseApp); signInWithPopup(auth, provider) .then(credential => { if (includeGoogleAdminScopes) { diff --git a/packages/firecms/src/index.ts b/packages/firecms/src/index.ts index 00e34984c..9153a7bbb 100644 --- a/packages/firecms/src/index.ts +++ b/packages/firecms/src/index.ts @@ -5,8 +5,8 @@ export * from "./utils"; export * from "./api/projects"; -export * from "./FireCMS3App"; -export type { FireCMS3AppProps } from "./FireCMS3AppProps"; +export * from "./FireCMSApp"; +export type { FireCMSAppProps } from "./FireCMSAppProps"; // we export everything in these packages for simplicity export * from "@firecms/firebase"; diff --git a/packages/firecms_cli/templates/template_v3/src/App.tsx b/packages/firecms_cli/templates/template_v3/src/App.tsx index 786b4fcb0..f58c55610 100644 --- a/packages/firecms_cli/templates/template_v3/src/App.tsx +++ b/packages/firecms_cli/templates/template_v3/src/App.tsx @@ -1,9 +1,9 @@ import React from "react" -import { FireCMS3App } from "firecms"; +import { FireCMSApp } from "firecms"; import appConfig from "./index"; function App() { - return ; diff --git a/packages/firecms_core/src/preview/PropertyPreview.tsx b/packages/firecms_core/src/preview/PropertyPreview.tsx index 767f2bf06..95247473c 100644 --- a/packages/firecms_core/src/preview/PropertyPreview.tsx +++ b/packages/firecms_core/src/preview/PropertyPreview.tsx @@ -211,7 +211,9 @@ export const PropertyPreview = React.memo(function PropertyPreview : content; + return content === undefined || content === null || (Array.isArray(content) && content.length === 0) + ? + : content; }, equal); function buildWrongValueType(name: string | undefined, dataType: string, value: any) { diff --git a/website/blog/2024-01-31-beta_launch_3_0.md b/website/blog/2024-01-31-beta_launch_3_0.md new file mode 100644 index 000000000..8f1851aba --- /dev/null +++ b/website/blog/2024-01-31-beta_launch_3_0.md @@ -0,0 +1,198 @@ +--- +slug: beta_launch_3_0 +title: Beta Launch, FireCMS 3.0 Cloud and Cloud Plus +image: /img/avatars/marian_avatar.jpeg +author_name: Marian Moldovan +author_url: https://www.linkedin.com/in/marianmoldovan/ +author_image_url: https://media.licdn.com/dms/image/C4E03AQFCD4YD1c8Uuw/profile-displayphoto-shrink_800_800/0/1655896232142?e=1710979200&v=beta&t=EckecOc3z-7s_6AaWW7rehbAOzrs4KTgOn73IivyLOA +--- + + +> ### We are giving away to our community a free a month of FireCMS Cloud Plus with the code **`IMONFIRE`**. + +## FireCMS 3.0 The Next CMS that is already here.πŸ”₯ + +Three years ago we launched the first version of FireCMS. We started offering an open source core so that anyone could +build whatever they need. FireCMS started as an internal tool that we offered to our clients. The usual use case is: you +are building an app or a web using Firebase as a backend, and need a backoffice tool to manage your data, your users, +your files. Everything in a secure environment and with a strong roles and permissions system. Along the years, we have +developed full stack solutions for partners like +as [Oikosbrain](https://oikosbrain.com/), [medicalmotion](https://medicalmotion.com/en) +or [ThePlanetApp](https://theplanetapp.com/?lang=en). Likewise, many agencies have benefited from the open-source nature +of the project and use it to develop internal apps for their own clients. Since it’s inception, FireCMS has been +downloaded 200k+ times in NPM. + +Along the years we understood that we need to make it simple for the customer to install and configure FireCMS, but also +keep the power of using code to extend the base product with any feature. We helped customers build extensions for other +database providers, such as MongoDB, or create a 3D model editor using Three.js. And that’s our focus for the next +years: Build the best no-code CMS, while preserving all the customization options in code, and ease of use our users +love. + +## Today we launch the Beta version of FireCMS 3 + +We have put a lot of effort into building a hosted version of the collection editor, including most of the customization +an validation oprions. We provide a great UI, and you extend it however you need. + + + +### ☁️ The Cloud Version + +One of the most significant additions to FireCMS 3.0 is the introduction of the SaaS cloud-hosted version. This new +offering allows users to easily access and manage their FireCMS projects directly from the cloud. With this feature, +users no longer need to worry about setting up and maintaining their own infrastructure, as FireCMS takes care of it +all. This greatly simplifies the deployment process and allows end users to focus more on building their applications. + +You no longer need to manually update CMS versions since every update is pushed to the cloud and received by all +clients. + +### πŸ”‘ The customers still own the data + +Your data is yours, we don’t need to access it. Each FireCMS project is linked to a Firebase project, that is owned by +you, the customer. We just provide the interface to manage the data. If you need, we also help you creating and +configuring the Google Cloud project and the Firebase project. We can help you with the process. Starting now. + +### πŸ§‘β€πŸ’» Customization with Code + +With FireCMS 3.0, customization options have been taken to the next level. Now, users can leverage the power of code to +personalize their CMS even further. Whether it's tweaking the layout, adding custom fields, or implementing complex +business logic, developers have the flexibility to extend and modify FireCMS to suit their specific needs. This level of +customization empowers users to create truly unique and tailored CMS experiences. We even built +a [CLI](https://firecms.co/docs/deployment##firecms-cli) for that. Start the project, write the code, upload it and find +the features in +the [app](https://www.notion.so/Beta-Launch-FireCMS-3-0-Cloud-and-Cloud-Plus-c99f446121614ae5bb832e8123ef071c?pvs=21). +Extend the React basecode with Typescript or Javascript. + +![An example of highly customized instance of FireCMS, with the ThreeJS editor integrated](../static/img/blog/overlay-6cd67e9930912d6032204e3f9b253171.webp) +> An example of highly customized instance of FireCMS, with the ThreeJS editor integrated + +We have also built a [CLI](https://firecms.co/docs/deployment##firecms-cli) for making the interaction with the Cloud +easier. Start the project, write the code, upload it and find the features in +the [app](https://www.notion.so/Beta-Launch-FireCMS-3-0-Cloud-and-Cloud-Plus-c99f446121614ae5bb832e8123ef071c?pvs=21). +Extend the React base code with Typescript or Javascript. + +And just deploy with one command: + +`firecms deploy` + +### New UI collection schema editor + +Originally, collections were only defined in code. Now, you can do it with a UI as well. Add fields, add collections, +property types, files, images. All the properties, now they can be configured with a few clicks. If you need to add some +logic to your collections, like enabling some fields based on other values, you can get the code for your current +collection + +![FireCMS collection editor](../static/img/blog/editor.png) + +But it gets even better! FireCMS can automatically detect new values and add them to the schema with just a single +click. This empowers users to easily customize and adapt their collection schemas to meet their evolving needs. + +Even though the collection schema is now stored in the backend, you still maintain full control over which properties +can be modified from the user interface. Moreover, you have the ability to define default values for new documents, +ensuring consistency and efficiency in data management. + +### New data inference[](https://firecms.co/docs/what_is_new_v3##new-data-inference) + +Do you have a few collections in your project and want to get started quickly? FireCMS can now **infer the schema from +your data**. This means that you can begin using FireCMS in just a few minutes, without the need to write a single line +of code. Simply import a data file and start immediately. + +### πŸ’½ Data import and export[](https://firecms.co/docs/what_is_new_v3##data-import-and-export) + +Now, you can import and export the data. As we've stated, you are the owner of the data and it's up to you to decide. + +#### Import[](https://firecms.co/docs/what_is_new_v3##import) + +You can now import data from CSV, JSON, or Excel files. This feature is excellent for migrating data from other systems. +We have incorporated a modern UI that enables you to define how the data is imported and how it is mapped to your +collections. + +![Import data view](../static/img/blog/import.png) + +#### Export + +You now have better control over how your data is **exported**: + +- Define the format of your timestamps. +- Define how arrays are serialized in CSV files (assigning one column per array item, or serializing the array as a + string). +- Export your data as JSON or CSV. + +### πŸ€– GPT-4 Content Generation + +We have improved our interface to enable content generation. You can create a complete database row from a prompt, a +title, or any available field. We also provide a UI to perform various actions such as summarizing, translating, and +expanding. Whatever you can think of is ready to use. We use GPT-4 under the hood. + +### 🚀 Performance improvements + +Versions 1.0 and 2.0 of FireCMS were based on Material UI (mui). This was great for getting started quickly, but it had +some drawbacks. The main one was that performance was not great. The styling solution of MUI is based on emotion which +resolves styles at runtime. This means that the browser has to do a lot of work to resolve the styles. This is not a +problem for small applications, but it can be a problem for large applications. + +In FireCMS 3.0, we have migrated to Tailwind CSS. This is a utility-first CSS framework that allows us to generate a +small CSS file with all the styles resolved at build time. This means that the browser does not have to do any work to +resolve the styles, which results in a much faster experience. πŸš€ + +### πŸ‘₯ Roles and permissions + +Now you can leverage out of the box invitations, user management, and a role system. The default ones, Viewer, Editor, +or Admin, or create your own custom ones. + +### πŸ” Local text search + +Search out of the box. No need to create an index or to rely on a third-party provider, type and find your records. We +look through all the fields and perform a smart search. Just click on search and start typing. + +### 🎨 New components library, theming and better icons + +In FireCMS 3.0, components, icons and colors have received a significant upgrade. + +We have built a full component library + +The new release includes an expanded set of icons, giving users more options to choose from when designing their CMS. +Additionally, the color palette has been expanded, allowing for greater flexibility in creating visually appealing +interfaces. These enhancements enable developers to create stunning and intuitive CMS designs that align with their +brand and aesthetic preferences. + +### Pricing + +We decided to provide a free tier of use of the CMS. **FireCMS Cloud Free**. This is everything you have in the Free +version: + +- Unlimited projects +- Unlimited collections +- All available form fields +- Schema editor and inference from data +- Advanced data import and export +- Default roles +- 3 users + +Regarding the premium option, we called it **FireCMS Cloud Plus**, and this is everything it comes with (for now): + +- Everything in the free tier +- Local text search +- Custom fields and custom views +- Unlimited users and roles +- Unlimited data export +- Theme and logo customization +- Customization with Code +- Custom user roles +- GPT-4 content generation + +While the free plan is, well, free, the Plus is 9,99€ per customer per month. We are continuously going to add features, +provide support, and a live web app where you can find your projects. + +### How to start? + +Go to [app.firecms.co](http://app.firecms.co/) and get started. + +We are giving away a free month of Plus customers to everyone using the code **IMONFIRE** at the checkout. + +### What are you going to build? + +We want to hear about it :) Find us +on [Discord](https://discord.gg/fxy7xsQm3m), [LinkedIn](https://www.linkedin.com/company/firecms/?originalSubdomain=es), +or ping us at [hello@firecms.co](mailto:hello@firecms.co) diff --git a/website/docs/collections/multiple_filters.md b/website/docs/collections/multiple_filters.md index 407a92a55..a3e1bb326 100644 --- a/website/docs/collections/multiple_filters.md +++ b/website/docs/collections/multiple_filters.md @@ -24,7 +24,7 @@ This callback will be called with the current path of the collection being rende You can then return an array of indexes that will be used to filter the collection. ```tsx -import { FireCMS3App, FirestoreIndexesBuilder } from "firecms"; +import { FireCMSApp, FirestoreIndexesBuilder } from "firecms"; // Sample index builder that allows filtering by `category` and `available` for the `products` collection const firestoreIndexesBuilder: FirestoreIndexesBuilder = ({ path }) => { @@ -55,7 +55,7 @@ const firestoreIndexesBuilder: FirestoreIndexesBuilder = ({ path }) => { // Add your indexes builder to your app function MyApp() { - return {CollectionBuilder} diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 924a41e6d..33a44696b 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -1,6 +1,7 @@ const fontaine = require("fontaine"); const path = require("path"); -import {themes as prismThemes} from 'prism-react-renderer'; +import { themes as prismThemes } from "prism-react-renderer"; + require("dotenv").config(); module.exports = { @@ -77,9 +78,10 @@ module.exports = { ], themeConfig: { image: "img/logo_small.png", + description: "Awesome headless CMS based Firestore/Firebase and React, and completely open-source", announcementBar: { - id: "local-search", - content: "Local text search is now available for PLUS users! πŸŽ‰", + id: "beta-3", + content: "FireCMS 3.0 is now in BETA Jump in! πŸŽ‰", backgroundColor: "#FF5B79", textColor: "black", isCloseable: true, @@ -112,7 +114,7 @@ module.exports = { ], colorMode: { - defaultMode: "dark", + defaultMode: "light", disableSwitch: false, // respectPrefersColorScheme: false, }, @@ -300,7 +302,7 @@ module.exports = { ] } ], - copyright: `MIT Β© ${new Date().getFullYear()} - FireCMS S.L.` + copyright: `Β© ${new Date().getFullYear()} - FireCMS S.L.` }, prism: { theme: prismThemes.vsDark, diff --git a/website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx b/website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx index cee0094d7..65b73952d 100644 --- a/website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx +++ b/website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx @@ -1,4 +1,4 @@ -import { buildCollection, CMSView, FireCMS3App, FireCMSAppConfig } from "firecms"; +import { buildCollection, CMSView, FireCMSApp, FireCMSAppConfig } from "firecms"; import { ExampleCMSView } from "./ExampleCMSView"; const projectId = "YOUR_PROJECT_ID"; @@ -33,7 +33,7 @@ const appConfig: FireCMSAppConfig = { export default function App() { - return ; diff --git a/website/samples/samples_v3/nextjs.tsx b/website/samples/samples_v3/nextjs.tsx index 4f071df57..ce2be1c7c 100644 --- a/website/samples/samples_v3/nextjs.tsx +++ b/website/samples/samples_v3/nextjs.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { buildCollection, buildProperty, EntityReference, FireCMS3App, } from "firecms"; +import { buildCollection, buildProperty, EntityReference, FireCMSApp, } from "firecms"; import "typeface-rubik"; import "@fontsource/ibm-plex-mono"; @@ -176,7 +176,7 @@ const productsCollection = buildCollection({ export default function CMS() { - return * { + padding: 16px; + z-index: 10; + border-radius: 8px; + background-color: var(--ifm-background-color); + border: 1px solid var(--ifm-border-color); +} -.table-of-contents.table-of-contents__left-border { +aside > * { + padding: 16px; + z-index: 10; border-radius: 8px; background-color: var(--ifm-background-color); border: 1px solid var(--ifm-border-color); @@ -341,7 +360,12 @@ p{ .theme-doc-markdown li { display: list-item; - list-style: disc; + list-style: initial; +} + +.markdown li { + word-wrap: break-word; + list-style: initial; } ul { diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index c1ca776a4..0061940fb 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -24,6 +24,9 @@ function Home() { description="Awesome headless CMS based Firestore/Firebase and React, and completely open-source"> FireCMS - Firestore/Firebase headless CMS + + + diff --git a/website/src/partials/features/DeveloperFeatures.tsx b/website/src/partials/features/DeveloperFeatures.tsx index 3182ee382..74e06b268 100644 --- a/website/src/partials/features/DeveloperFeatures.tsx +++ b/website/src/partials/features/DeveloperFeatures.tsx @@ -122,7 +122,7 @@ export function DeveloperFeatures() {

- Built for every project + 0Built for every project

diff --git a/website/static/img/blog/demo_books_llm.webm b/website/static/img/blog/demo_books_llm.webm new file mode 100644 index 000000000..39c76f866 Binary files /dev/null and b/website/static/img/blog/demo_books_llm.webm differ diff --git a/website/static/img/blog/editor.png b/website/static/img/blog/editor.png new file mode 100644 index 000000000..3d910c868 Binary files /dev/null and b/website/static/img/blog/editor.png differ diff --git a/website/static/img/blog/import.png b/website/static/img/blog/import.png new file mode 100644 index 000000000..bcd29d78b Binary files /dev/null and b/website/static/img/blog/import.png differ diff --git a/website/static/img/blog/overlay-6cd67e9930912d6032204e3f9b253171.webp b/website/static/img/blog/overlay-6cd67e9930912d6032204e3f9b253171.webp new file mode 100644 index 000000000..9c1c867b0 Binary files /dev/null and b/website/static/img/blog/overlay-6cd67e9930912d6032204e3f9b253171.webp differ