diff --git a/README.md b/README.md index 49a886d75..0580c5c50 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ data. ### ✨ Robust Forms -![fields](https://firecms.co/img/post_editing.png) +![fields](https://firecms.co/img/post_editing.webp) When editing an entity, FireCMS offers a nested system of side dialogs for navigating through **subcollections** and accessing custom views (such as custom diff --git a/examples/example_pro/src/SampleApp/SampleApp.tsx b/examples/example_pro/src/SampleApp/SampleApp.tsx index d6dcfe476..5e700f5da 100644 --- a/examples/example_pro/src/SampleApp/SampleApp.tsx +++ b/examples/example_pro/src/SampleApp/SampleApp.tsx @@ -7,17 +7,8 @@ import "@fontsource/roboto" import { getAnalytics, logEvent } from "firebase/analytics"; import { User as FirebaseUser } from "firebase/auth"; -import { - CMSView, - GitHubIcon, - IconButton, - Tooltip -} from "@firecms/core"; -import { - Authenticator, - FirebaseCMSApp, - FirestoreIndexesBuilder, -} from "@firecms/firebase_pro"; +import { CMSView, GitHubIcon, IconButton, Tooltip } from "@firecms/core"; +import { Authenticator, FireCMSProApp, FirestoreIndexesBuilder, } from "@firecms/firebase_pro"; import { useDataEnhancementPlugin } from "@firecms/data_enhancement"; import { firebaseConfig } from "../firebase_config"; @@ -149,7 +140,7 @@ function SampleApp() { return undefined; } - return { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); const collectionReference: Query = collectionGroup ? collectionGroupClause(firestore, path) : collectionClause(firestore, path); @@ -132,7 +132,7 @@ export function useFirestoreDelegate({ const getAndBuildEntity = useCallback(>(path: string, entityId: string ): Promise | undefined> => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); @@ -222,7 +222,7 @@ export function useFirestoreDelegate({ return new FirestoreGeoPoint(geoPoint.latitude, geoPoint.longitude) }, buildReference(reference: EntityReference): any { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); return doc(firestore, reference.path, reference.id); }, @@ -370,7 +370,7 @@ export function useFirestoreDelegate({ onUpdate, onError }: ListenEntityProps): () => void => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); @@ -404,7 +404,7 @@ export function useFirestoreDelegate({ status }: SaveEntityProps): Promise> => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); @@ -437,7 +437,7 @@ export function useFirestoreDelegate({ entity }: DeleteEntityProps ): Promise => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); @@ -461,7 +461,7 @@ export function useFirestoreDelegate({ entityId?: string ): Promise => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); @@ -479,7 +479,7 @@ export function useFirestoreDelegate({ }, [firebaseApp]), generateEntityId: useCallback((path: string): string => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const firestore = getFirestore(firebaseApp); return doc(collectionClause(firestore, path)).id; }, [firebaseApp]), @@ -497,7 +497,7 @@ export function useFirestoreDelegate({ order?: "desc" | "asc", isCollectionGroup?: boolean }): Promise => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const query = buildQuery(path, filter, orderBy, order, undefined, undefined, isCollectionGroup); const snapshot = await getCountFromServer(query); return snapshot.data().count; @@ -514,7 +514,7 @@ export function useFirestoreDelegate({ filterValues: FilterValues, sortBy?: [string, "asc" | "desc"] }): boolean => { - if (!firebaseApp) throw Error("useFirestoreDataSource Firebase not initialised"); + if (!firebaseApp) throw Error("useFirestoreDelegate Firebase not initialised"); const indexes = firestoreIndexesBuilder?.({ path, diff --git a/packages/firebase_firecms/src/utils/index.ts b/packages/firebase_firecms/src/utils/index.ts index 050c513d4..8959ec97b 100644 --- a/packages/firebase_firecms/src/utils/index.ts +++ b/packages/firebase_firecms/src/utils/index.ts @@ -1,3 +1,2 @@ export * from "./collections_firestore"; export * from "./database"; -export * from "./local_storage"; diff --git a/packages/firebase_firecms_pro/src/FireCMSProApp.tsx b/packages/firebase_firecms_pro/src/FireCMSProApp.tsx index 40b69edcd..3c35ac386 100644 --- a/packages/firebase_firecms_pro/src/FireCMSProApp.tsx +++ b/packages/firebase_firecms_pro/src/FireCMSProApp.tsx @@ -18,7 +18,7 @@ import { useBuildModeController } from "@firecms/core"; -import { FirebaseCMSAppProps } from "./FirebaseCMSAppProps"; +import { FireCMSProAppProps } from "./FireCMSProAppProps"; import { FirebaseLoginView } from "./components/FirebaseLoginView"; import { FirebaseAuthController, @@ -51,7 +51,7 @@ const DEFAULT_SIGN_IN_OPTIONS = [ * @constructor * @category Firebase */ -export function FirebaseCMSApp({ +export function FireCMSProApp({ name, logo, logoDark, @@ -75,7 +75,7 @@ export function FirebaseCMSApp({ autoOpenDrawer, firestoreIndexesBuilder, components - }: FirebaseCMSAppProps) { + }: FireCMSProAppProps) { /** * Update the browser title and icon diff --git a/packages/firebase_firecms_pro/src/FireCMSProAppProps.tsx b/packages/firebase_firecms_pro/src/FireCMSProAppProps.tsx index fe6730b55..2ec3e901f 100644 --- a/packages/firebase_firecms_pro/src/FireCMSProAppProps.tsx +++ b/packages/firebase_firecms_pro/src/FireCMSProAppProps.tsx @@ -26,7 +26,7 @@ import { * Main entry point that defines the CMS configuration * @category Firebase */ -export type FirebaseCMSAppProps = { +export type FireCMSProAppProps = { /** * Name of the app, displayed as the main title and in the tab title diff --git a/packages/firebase_firecms_pro/src/index.ts b/packages/firebase_firecms_pro/src/index.ts index b57f08380..e7502ac68 100644 --- a/packages/firebase_firecms_pro/src/index.ts +++ b/packages/firebase_firecms_pro/src/index.ts @@ -1,5 +1,5 @@ -export { FirebaseCMSApp } from "./FirebaseCMSApp"; -export type { FirebaseCMSAppProps } from "./FirebaseCMSAppProps"; +export { FireCMSProApp } from "./FireCMSProApp"; +export type { FireCMSProAppProps } from "./FireCMSProAppProps"; export type { FirebaseLoginViewProps } from "./components/FirebaseLoginView"; export { FirebaseLoginView, LoginButton } from "./components/FirebaseLoginView"; diff --git a/packages/firecms/package.json b/packages/firecms/package.json index f1a4a6071..28ff82f83 100644 --- a/packages/firecms/package.json +++ b/packages/firecms/package.json @@ -61,5 +61,5 @@ "react-app/jest" ] }, - "gitHead": "80d437634caccc4400fa111504186eb13197bac1" + "gitHead": "bfe341a3a8e3d2b33d4dc3f48fabc350eccace1b" } diff --git a/packages/firecms/src/components/subscriptions/PlansComparison.tsx b/packages/firecms/src/components/subscriptions/PlansComparison.tsx index 88e5fe923..4b37bfead 100644 --- a/packages/firecms/src/components/subscriptions/PlansComparison.tsx +++ b/packages/firecms/src/components/subscriptions/PlansComparison.tsx @@ -153,6 +153,7 @@ export function PlansComparison() {
  • Everything in PLUS
  • SAML SSO
  • Custom domain
  • +
  • Full CMS components customization
  • Priority support
  • Roadmap prioritization
  • diff --git a/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx b/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx index af7ba2143..5625efb25 100644 --- a/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx +++ b/packages/firecms/src/hooks/useBuildFireCMSBackend.tsx @@ -13,7 +13,7 @@ import { } from "firebase/auth"; import { useCallback, useEffect, useRef, useState } from "react"; import { buildProjectsApi } from "../api/projects"; -import { clearDelegatedLoginTokensCache, FirebaseSignInOption, FirebaseSignInProvider } from "@firecms/firebase"; +import { clearDelegatedLoginTokensCache } from "../utils"; const AUTH_SCOPES = [ "https://www.googleapis.com/auth/cloud-platform" diff --git a/packages/firecms/src/hooks/useDelegatedLogin.tsx b/packages/firecms/src/hooks/useDelegatedLogin.tsx index 34a9b592a..6152d0c7b 100644 --- a/packages/firecms/src/hooks/useDelegatedLogin.tsx +++ b/packages/firecms/src/hooks/useDelegatedLogin.tsx @@ -2,7 +2,7 @@ import { FirebaseApp } from "firebase/app"; import { useCallback, useEffect, useState } from "react"; import { getAuth, signInWithCustomToken, User as FirebaseUser } from "firebase/auth"; import { ProjectsApi } from "../api/projects"; -import { cacheDelegatedLoginToken, getDelegatedLoginTokenFromCache } from "@firecms/firebase"; +import { cacheDelegatedLoginToken, getDelegatedLoginTokenFromCache } from "../utils"; export type DelegatedLoginProps = { projectsApi: ProjectsApi; diff --git a/packages/firecms/src/utils/index.ts b/packages/firecms/src/utils/index.ts index da523e6d6..968241b30 100644 --- a/packages/firecms/src/utils/index.ts +++ b/packages/firecms/src/utils/index.ts @@ -1,3 +1,3 @@ export * from "./permissions"; export * from "./admin_views"; -export * from "./firecms_tailwind_plugin"; +export * from "./local_storage"; diff --git a/packages/firecms/test/TestCMS3App.tsx b/packages/firecms/test/TestCMS3App.tsx index 8a4594fed..6aebda975 100644 --- a/packages/firecms/test/TestCMS3App.tsx +++ b/packages/firecms/test/TestCMS3App.tsx @@ -32,26 +32,15 @@ import { PersistedCollection, useCollectionEditorPlugin } from "@firecms/collection_editor"; -import { useDataEnhancementPlugin } from "@firecms/data_enhancement"; -import { - FireCMSBackEndProvider, - ProjectConfig, - ProjectConfigProvider, - useFirebaseStorageSource, - useFirestoreDataSource -} from "../src/hooks"; +import { FireCMSBackEndProvider, ProjectConfig, ProjectConfigProvider } from "../src/hooks"; import { FireCMS3AppProps } from "../src/FireCMS3AppProps"; import { - FirebaseAuthController, - FirebaseSignInOption, - FirebaseSignInProvider, - FireCMSBackend, FireCMSAppConfig, + FireCMSBackend, FireCMSUser } from "../src/types"; -import { FirestoreTextSearchController } from "../src/types/text_search"; import { ADMIN_VIEWS, getUserRoles, @@ -59,10 +48,8 @@ import { resolveCollectionConfigPermissions, resolveUserRolePermissions } from "../src/utils"; -import { FireCMSDataEnhancementSubscriptionMessage, FireCMSDrawer, FireCMSLoginView } from "../src/components"; -import { buildCollectionInference } from "../src/collection_editor/infer_collection"; +import { FireCMSDrawer, FireCMSLoginView } from "../src/components"; import { FireCMSProjectHomePage } from "../src/components/FireCMSProjectHomePage"; -import { getFirestoreDataInPath } from "../src/utils/database"; import { useImportExportPlugin } from "../src/hooks/useImportExportPlugin"; import { useBuildMockFireCMSBackend } from "./mocks/useBuildMockFireCMSBackend"; import { useBuildMockProjectConfig } from "./mocks/useBuildMockProjectConfig"; @@ -71,6 +58,7 @@ import { useMockDelegatedLogin } from "./mocks/useDelegatedLogin"; import { useBuildMockCollectionsConfigController } from "./mocks/useBuildMockCollectionsConfigController"; import { useBuildMockDataSource } from "./mocks/useBuildMockDataSource"; import { useBuildMockStorageSource } from "./mocks/useBuildMockStorageSource"; +import { FirebaseAuthController, FirebaseSignInOption, FirebaseSignInProvider, FirestoreTextSearchController } from "@firecms/firebase"; /** * This is the default implementation of a FireCMS app using the Firebase services diff --git a/packages/firecms_cli/package.json b/packages/firecms_cli/package.json index 7cca06434..5a9e7813d 100644 --- a/packages/firecms_cli/package.json +++ b/packages/firecms_cli/package.json @@ -66,5 +66,5 @@ "node_modules", "template/node_modules" ], - "gitHead": "80d437634caccc4400fa111504186eb13197bac1" + "gitHead": "bfe341a3a8e3d2b33d4dc3f48fabc350eccace1b" } diff --git a/packages/firecms_cli/src/commands/init.ts b/packages/firecms_cli/src/commands/init.ts index 3f31ed6ff..225b393ad 100644 --- a/packages/firecms_cli/src/commands/init.ts +++ b/packages/firecms_cli/src/commands/init.ts @@ -351,7 +351,6 @@ async function getProjects(env: "prod" | "dev", onErr?: (e: any) => void) { }, }); - console.log("status", response.status); if (response.status >= 400) { console.log(response.data.data?.message); return null; diff --git a/packages/firecms_cli/templates/template_v3/package.json b/packages/firecms_cli/templates/template_v3/package.json index ca5477d33..d71e130a3 100644 --- a/packages/firecms_cli/templates/template_v3/package.json +++ b/packages/firecms_cli/templates/template_v3/package.json @@ -10,7 +10,7 @@ "deploy": "run-s build && firecms deploy --project=[REPLACE_WITH_PROJECT_ID]" }, "dependencies": { - "@firecms/firebase": "^3.0.0-alpha.23", + "firecms": "^3.0.0-alpha.36", "firebase": "^10.5.2", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/packages/firecms_cli/templates/template_v3/src/App.tsx b/packages/firecms_cli/templates/template_v3/src/App.tsx index bac11302c..786b4fcb0 100644 --- a/packages/firecms_cli/templates/template_v3/src/App.tsx +++ b/packages/firecms_cli/templates/template_v3/src/App.tsx @@ -1,5 +1,5 @@ import React from "react" -import { FireCMS3App } from "@firecms/firebase"; +import { FireCMS3App } from "firecms"; import appConfig from "./index"; function App() { diff --git a/packages/firecms_cli/templates/template_v3/src/collections/demo.tsx b/packages/firecms_cli/templates/template_v3/src/collections/demo.tsx index 3ac5c4c61..255336a2a 100644 --- a/packages/firecms_cli/templates/template_v3/src/collections/demo.tsx +++ b/packages/firecms_cli/templates/template_v3/src/collections/demo.tsx @@ -1,4 +1,4 @@ -import { buildCollection, buildProperty } from "@firecms/firebase"; +import { buildCollection, buildProperty } from "firecms"; // This is a demo collection with many of the available properties // Note t diff --git a/packages/firecms_cli/templates/template_v3/src/entity_views/SampleEntityView.tsx b/packages/firecms_cli/templates/template_v3/src/entity_views/SampleEntityView.tsx index afd6b4d28..1c08a5304 100644 --- a/packages/firecms_cli/templates/template_v3/src/entity_views/SampleEntityView.tsx +++ b/packages/firecms_cli/templates/template_v3/src/entity_views/SampleEntityView.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Button, Entity, EntityValues, useSnackbarController } from "@firecms/firebase"; +import { Button, Entity, EntityValues, useSnackbarController } from "firecms"; import { Product } from "../types"; export function SampleEntityView({ diff --git a/packages/firecms_cli/templates/template_v3/src/form_fields/CustomColorTextField.tsx b/packages/firecms_cli/templates/template_v3/src/form_fields/CustomColorTextField.tsx index ee2ac33d5..f88cf65b5 100644 --- a/packages/firecms_cli/templates/template_v3/src/form_fields/CustomColorTextField.tsx +++ b/packages/firecms_cli/templates/template_v3/src/form_fields/CustomColorTextField.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { FieldHelperText, FieldProps, TextField, useModeController } from "@firecms/firebase"; +import { FieldHelperText, FieldProps, TextField, useModeController } from "firecms"; interface CustomColorTextFieldProps { color: string diff --git a/packages/firecms_cli/templates/template_v3/src/index.tsx b/packages/firecms_cli/templates/template_v3/src/index.tsx index 3dc40491c..d3646e5c5 100644 --- a/packages/firecms_cli/templates/template_v3/src/index.tsx +++ b/packages/firecms_cli/templates/template_v3/src/index.tsx @@ -1,4 +1,4 @@ -import { FireCMSAppConfig } from "@firecms/firebase"; +import { FireCMSAppConfig } from "firecms"; import { SampleEntityView } from "./entity_views/SampleEntityView"; import { demoCollection } from "./collections/demo"; diff --git a/packages/firecms_cli/templates/template_v3/src/types.ts b/packages/firecms_cli/templates/template_v3/src/types.ts index 177ff78ec..5bcb2e398 100644 --- a/packages/firecms_cli/templates/template_v3/src/types.ts +++ b/packages/firecms_cli/templates/template_v3/src/types.ts @@ -1,4 +1,4 @@ -import { EntityReference } from "@firecms/firebase"; +import { EntityReference } from "firecms"; export type Product = { name: string; diff --git a/packages/firecms_core/README.md b/packages/firecms_core/README.md index 0b97d6dd3..6b4aa7157 100644 --- a/packages/firecms_core/README.md +++ b/packages/firecms_core/README.md @@ -83,7 +83,7 @@ data. ### ✨ Robust Forms -![fields](https://firecms.co/img/post_editing.png) +![fields](https://firecms.co/img/post_editing.webp) When editing an entity, FireCMS offers a nested system of side dialogs for navigating through **subcollections** and accessing custom views (such as custom diff --git a/packages/firecms_core/package.json b/packages/firecms_core/package.json index 751aec86a..68cd4a3b2 100644 --- a/packages/firecms_core/package.json +++ b/packages/firecms_core/package.json @@ -134,7 +134,7 @@ "dist", "src" ], - "gitHead": "80d437634caccc4400fa111504186eb13197bac1", + "gitHead": "bfe341a3a8e3d2b33d4dc3f48fabc350eccace1b", "publishConfig": { "access": "public" } diff --git a/packages/schema_inference/package.json b/packages/schema_inference/package.json index 522761571..f4a78957e 100644 --- a/packages/schema_inference/package.json +++ b/packages/schema_inference/package.json @@ -29,5 +29,5 @@ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json", "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f" }, - "gitHead": "80d437634caccc4400fa111504186eb13197bac1" + "gitHead": "bfe341a3a8e3d2b33d4dc3f48fabc350eccace1b" } diff --git a/website/.yarn/cache/clean-css-npm-5.3.2-8946cefff9-8787b281ac.zip b/website/.yarn/cache/clean-css-npm-5.3.2-8946cefff9-8787b281ac.zip deleted file mode 100644 index afeb78f2f..000000000 Binary files a/website/.yarn/cache/clean-css-npm-5.3.2-8946cefff9-8787b281ac.zip and /dev/null differ diff --git a/website/.yarn/cache/ignore-npm-5.2.4-fbe6e989e5-3d4c309c60.zip b/website/.yarn/cache/ignore-npm-5.2.4-fbe6e989e5-3d4c309c60.zip deleted file mode 100644 index 50627d8e1..000000000 Binary files a/website/.yarn/cache/ignore-npm-5.2.4-fbe6e989e5-3d4c309c60.zip and /dev/null differ diff --git a/website/.yarn/cache/minipass-collect-npm-1.0.2-3b4676eab5-14df761028.zip b/website/.yarn/cache/minipass-collect-npm-1.0.2-3b4676eab5-14df761028.zip deleted file mode 100644 index 582f61ca2..000000000 Binary files a/website/.yarn/cache/minipass-collect-npm-1.0.2-3b4676eab5-14df761028.zip and /dev/null differ diff --git a/website/.yarn/cache/object.assign-npm-4.1.4-fb3deb1c3a-76cab513a5.zip b/website/.yarn/cache/object.assign-npm-4.1.4-fb3deb1c3a-76cab513a5.zip deleted file mode 100644 index 8a1fef055..000000000 Binary files a/website/.yarn/cache/object.assign-npm-4.1.4-fb3deb1c3a-76cab513a5.zip and /dev/null differ diff --git a/website/.yarn/cache/sass-loader-npm-10.4.1-2a13914f69-df9a65a622.zip b/website/.yarn/cache/sass-loader-npm-10.4.1-2a13914f69-df9a65a622.zip deleted file mode 100644 index 675c0d86d..000000000 Binary files a/website/.yarn/cache/sass-loader-npm-10.4.1-2a13914f69-df9a65a622.zip and /dev/null differ diff --git a/website/.yarn/install-state.gz b/website/.yarn/install-state.gz index 0f4379137..628d7046f 100644 Binary files a/website/.yarn/install-state.gz and b/website/.yarn/install-state.gz differ diff --git a/website/blog/2023-05-28-why_react_cms.md b/website/blog/2023-05-28-why_react_cms.md index 29509906a..1c7e35fb7 100644 --- a/website/blog/2023-05-28-why_react_cms.md +++ b/website/blog/2023-05-28-why_react_cms.md @@ -37,7 +37,7 @@ process of creating interactive and lightning-fast user interfaces. As such, integrating a React-based headless CMS into your projects unlocks multiple benefits, including: -![blog_example](/img/blog_example.png) +![blog_example](/img/blog_example.webp) ### 1. Reusable Components diff --git a/website/docs/collections/text_search.md b/website/docs/collections/text_search.md index 3c6be090a..f16f0a086 100644 --- a/website/docs/collections/text_search.md +++ b/website/docs/collections/text_search.md @@ -13,9 +13,8 @@ Firestore does not support native text search, so we need to rely on external solutions. If you specify a `textSearchEnabled` flag to the **collection**, you will see a search bar on top of the collection view. -You need to define a `FirestoreTextSearchController` and pass it to your -`FireCMS3App` component (or `useFirestoreDataSource` if you are building a -custom app). Typically, you will want to index your entities in some external +You need to define a `FirestoreTextSearchController` and pass it to your app config. +Typically, you will want to index your entities in some external solution, such as Algolia. For this to work you need to set up an AlgoliaSearch account and manage the indexing of your documents. diff --git a/website/docs/custom_top_level_views.mdx b/website/docs/custom_top_level_views.mdx index 027269561..afa3a2760 100644 --- a/website/docs/custom_top_level_views.mdx +++ b/website/docs/custom_top_level_views.mdx @@ -48,7 +48,7 @@ A quick example for a custom view: import CodeBlock from "@theme/CodeBlock"; import CustomViewSampleApp - from "!!raw-loader!../src/samples_v3/custom_cms_view/CustomViewSampleApp"; + from "!!raw-loader!../samples/samples_v3/custom_cms_view/CustomViewSampleApp"; {CustomViewSampleApp} @@ -57,7 +57,7 @@ Your custom view is implemented as any regular React component that uses some hooks provided by the CMS: import ExampleCMSView - from "!!raw-loader!../src/samples_v3/custom_cms_view/ExampleCMSView"; + from "!!raw-loader!../samples/samples_v3/custom_cms_view/ExampleCMSView"; {ExampleCMSView} diff --git a/website/docs/recipes/blog.mdx b/website/docs/recipes/blog.mdx index 4de488056..fa2a61b85 100644 --- a/website/docs/recipes/blog.mdx +++ b/website/docs/recipes/blog.mdx @@ -3,10 +3,10 @@ id: building_a_blog title: Building a blog sidebar_label: Building a blog description: Use FireCMS to build a blog, including field customization and preview. -image: /img/blog_example.png +image: /img/blog_example.webp --- -![blog_example](/img/blog_example.png) +![blog_example](/img/blog_example.webp) :::note In this tutorial we assume you have set up a Firebase project and a FireCMS @@ -346,6 +346,6 @@ we get the following code for the blog collection: import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../src/samples_v3/recipes/blog/blog_collection"; + from "!!raw-loader!../../samples/samples_v3/recipes/blog/blog_collection"; {MyComponentSource} diff --git a/website/docs/recipes/copy_entity.mdx b/website/docs/recipes/copy_entity.mdx index d50b00f60..f34663a0a 100644 --- a/website/docs/recipes/copy_entity.mdx +++ b/website/docs/recipes/copy_entity.mdx @@ -5,7 +5,7 @@ sidebar_label: Copying from another collection description: How to implement custom logic to copy an entity from one collection to another --- -![Product selection](/img/product_selection.png) +![Product selection](/img/product_selection.webp) :::note In this tutorial we assume you have set up a Firebase project and a FireCMS @@ -28,13 +28,13 @@ that we will be copying from and to: import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../src/samples_v3/recipes/copy_entity/simple_product_collection"; + from "!!raw-loader!../../samples/samples_v3/recipes/copy_entity/simple_product_collection"; import CopyButton - from "!!raw-loader!../../src/samples_v3/recipes/copy_entity/copy_button"; + from "!!raw-loader!../../samples/samples_v3/recipes/copy_entity/copy_button"; import CopyButtonUse - from "!!raw-loader!../../src/samples_v3/recipes/copy_entity/copy_button_use"; + from "!!raw-loader!../../samples/samples_v3/recipes/copy_entity/copy_button_use"; import FullCode - from "!!raw-loader!../../src/samples_v3/recipes/copy_entity/full"; + from "!!raw-loader!../../samples/samples_v3/recipes/copy_entity/full"; {MyComponentSource} diff --git a/website/docs/recipes/documents_as_subcollections.mdx b/website/docs/recipes/documents_as_subcollections.mdx index 6342ffa64..a83633483 100644 --- a/website/docs/recipes/documents_as_subcollections.mdx +++ b/website/docs/recipes/documents_as_subcollections.mdx @@ -39,11 +39,11 @@ How cool is that? import CodeBlock from "@theme/CodeBlock"; import UnitCollection - from "!!raw-loader!../../src/samples_v3/recipes/document_as_subcollection/unit_collection"; + from "!!raw-loader!../../samples/samples_v3/recipes/document_as_subcollection/unit_collection"; import CollectionBuilder - from "!!raw-loader!../../src/samples_v3/recipes/document_as_subcollection/collection_builder"; + from "!!raw-loader!../../samples/samples_v3/recipes/document_as_subcollection/collection_builder"; import FullCode - from "!!raw-loader!../../src/samples_v3/recipes/document_as_subcollection/full"; + from "!!raw-loader!../../samples/samples_v3/recipes/document_as_subcollection/full"; {UnitCollection} diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 78dfcbd0b..c528db597 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -22,12 +22,12 @@ module.exports = { plugins: [ "docusaurus-tailwindcss-loader", "docusaurus-plugin-sass", - [ - "docusaurus-plugin-typedoc", - { - watch: false, - } - ], + // [ + // "docusaurus-plugin-typedoc", + // { + // watch: false, + // } + // ], function fontainePlugin(_context, _options) { return { name: "fontaine-plugin", diff --git a/website/package.json b/website/package.json index a3b8c286b..f63a2a044 100644 --- a/website/package.json +++ b/website/package.json @@ -53,7 +53,6 @@ }, "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.3", - "@firecms/firebase": "^3.0.0-alpha.28", "@tsconfig/docusaurus": "^1.0.7", "@types/react": "^18.2.33", "@types/react-helmet": "^6.1.6", @@ -62,6 +61,7 @@ "docusaurus-plugin-typedoc": "^0.21.0", "docusaurus-tailwindcss-loader": "file:plugins/docusaurus-tailwindcss-loader", "esbuild-loader": "^4.0.2", + "firecms": "^3.0.0-alpha.28", "fontaine": "^0.4.0", "postcss": "^8.4.21", "postcss-import": "^15.1.0", diff --git a/website/samples/samples_v2/CustomCMSApp.tsx b/website/samples/samples_v2/CustomCMSApp.tsx new file mode 100644 index 000000000..1db7fad3e --- /dev/null +++ b/website/samples/samples_v2/CustomCMSApp.tsx @@ -0,0 +1,183 @@ +import React, { useMemo } from "react"; + +import { GoogleAuthProvider } from "firebase/auth"; +import { CssBaseline, ThemeProvider } from "@mui/material"; +import { BrowserRouter as Router } from "react-router-dom"; + +import "typeface-rubik"; +import "@fontsource/ibm-plex-mono"; + +import { + buildCollection, + CircularProgressCenter, + createCMSDefaultTheme, + FirebaseAuthController, + FirebaseLoginView, + FireCMS, + ModeControllerProvider, + NavigationRoutes, + Scaffold, + SideDialogs, + SnackbarProvider, + useBuildModeController, + useFirebaseAuthController, + useFirebaseStorageSource, + useFirestoreDataSource, + useInitialiseFirebase, + useValidateAuthenticator +} from "firecms"; + +import { firebaseConfig } from "../firebase_config"; + +const DEFAULT_SIGN_IN_OPTIONS = [ + GoogleAuthProvider.PROVIDER_ID +]; + +const productsCollection = buildCollection({ + path: "products", + permissions: ({ user }) => ({ + edit: true, + create: true, + delete: true + }), + name: "Products", + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + name: "Price", + validation: { + required: true, + requiredMessage: "You must set a price between 0 and 1000", + min: 0, + max: 1000 + }, + description: "Price with range validation", + dataType: "number" + }, + status: { + name: "Status", + validation: { required: true }, + dataType: "string", + description: "Should this product be visible in the website", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + enumValues: { + private: "Private", + public: "Public" + } + } + } +}); + +/** + * This is an example of how to use the components provided by FireCMS for + * a better customisation. + * @constructor + */ +export function CustomCMSApp() { + + const signInOptions = DEFAULT_SIGN_IN_OPTIONS; + + const { + firebaseApp, + firebaseConfigLoading, + configError, + firebaseConfigError + } = useInitialiseFirebase({ firebaseConfig }); + + const authController: FirebaseAuthController = useFirebaseAuthController({ + firebaseApp, + signInOptions + }); + + const dataSource = useFirestoreDataSource({ + firebaseApp + // You can add your `FirestoreTextSearchController` here + }); + + const storageSource = useFirebaseStorageSource({ firebaseApp }); + + const modeController = useBuildModeController(); + const theme = useMemo(() => createCMSDefaultTheme({ mode: modeController.mode }), []); + + const { + authLoading, + canAccessMainView, + notAllowedError + } = useValidateAuthenticator({ + authController, + authentication: async ({ user }) => { + console.log("Allowing access to", user?.email); + return true; + }, + dataSource, + storageSource + }); + + if (configError) { + return
    {configError}
    ; + } + + if (firebaseConfigError) { + return
    + It seems like the provided Firebase config is not correct. If you + are using the credentials provided automatically by Firebase + Hosting, make sure you link your Firebase app to Firebase + Hosting. +
    ; + } + + if (firebaseConfigLoading || !firebaseApp) { + return ; + } + + return ( + + + + `https://console.firebase.google.com/project/${firebaseApp.options.projectId}/firestore/data/${entity.path}/${entity.id}`} + > + {({ context, loading }) => { + + let component; + if (loading) { + component = ; + } else if (!canAccessMainView) { + component = ( + + ); + } else { + component = ( + + + + + ); + } + + return ( + + + {component} + + ); + }} + + + + + ); + +} diff --git a/website/samples/samples_v2/ExampleCMSView.tsx b/website/samples/samples_v2/ExampleCMSView.tsx new file mode 100644 index 000000000..2c029eee0 --- /dev/null +++ b/website/samples/samples_v2/ExampleCMSView.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { + Box, + Button, + Card, + CardActions, + CardContent, + Container, + Grid, + IconButton, + Paper, + Tooltip, + Typography +} from "@mui/material"; +import { GitHub } from "@mui/icons-material"; + +import { + buildCollection, + Entity, + EntityCollectionView, + useAuthController, + useReferenceDialog, + useSelectionController, + useSideEntityController, + useSnackbarController +} from "firecms"; +import { Product } from "./types"; +import { usersCollection } from "./collections/users_collection"; + +/** + * Sample CMS view not bound to a collection, customizable by the developer + * @constructor + */ +export function ExampleCMSView() { + + // hook to display custom snackbars + const snackbarController = useSnackbarController(); + + const selectionController = useSelectionController(); + + console.log("Selection from ExampleCMSView", selectionController.selectedEntities); + + // hook to open the side dialog that shows the entity forms + const sideEntityController = useSideEntityController(); + + // hook to do operations related to authentication + const authController = useAuthController(); + + // hook to open a reference dialog + const referenceDialog = useReferenceDialog({ + path: "products", + onSingleEntitySelected(entity: Entity | null) { + snackbarController.open({ + type: "success", + message: "Selected " + entity?.values.name + }) + } + }); + + const customProductCollection = buildCollection({ + path: "custom_product", + name: "Custom products", + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + very_custom_field: { + name: "Very custom field", + dataType: "string" + } + } + }); + + const githubLink = ( + + + + + + ); + + return ( + + + + + + + + + + + This is an example of an + additional view + + + {authController.user + ? <>Logged in + as {authController.user.displayName} + : <>You are not logged in} + + + + + + + + Use this button to select an entity + under + the path `products` programmatically + + + + + + + + + + + + + Use this button to open a snackbar + + + + + + + + + + + + + + Use this button to open an entity in a + custom path with a custom schema + + + + + + + + + + + + You can include full entity collections in + your views: + + + + + + + + + {githubLink} + + + + + + + + ); +} diff --git a/website/samples/samples_v2/SimpleApp.tsx b/website/samples/samples_v2/SimpleApp.tsx new file mode 100644 index 000000000..724402429 --- /dev/null +++ b/website/samples/samples_v2/SimpleApp.tsx @@ -0,0 +1,217 @@ +import React, { useCallback } from "react"; + +import { User as FirebaseUser } from "firebase/auth"; +import { + Authenticator, + buildCollection, + buildProperty, + EntityReference, + FirebaseCMSApp +} from "firecms"; + +import "typeface-rubik"; +import "@fontsource/ibm-plex-mono"; + +// TODO: Replace with your config +const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; + +const locales = { + "en-US": "English (United States)", + "es-ES": "Spanish (Spain)", + "de-DE": "German" +}; + +type Product = { + name: string; + price: number; + status: string; + published: boolean; + related_products: EntityReference[]; + main_image: string; + tags: string[]; + description: string; + categories: string[]; + publisher: { + name: string; + external_id: string; + }, + expires_on: Date +} + +const localeCollection = buildCollection({ + path: "locale", + customId: locales, + name: "Locales", + singularName: "Locales", + properties: { + name: { + name: "Title", + validation: { required: true }, + dataType: "string" + }, + selectable: { + name: "Selectable", + description: "Is this locale selectable", + dataType: "boolean" + } + } +}); + +const productsCollection = buildCollection({ + name: "Products", + singularName: "Product", + path: "products", + permissions: ({ authController }) => ({ + read: true, + edit: true, + create: true, + delete: true + }), + subcollections: [ + localeCollection + ], + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + name: "Price", + validation: { + required: true, + requiredMessage: "You must set a price between 0 and 1000", + min: 0, + max: 1000 + }, + description: "Price with range validation", + dataType: "number" + }, + status: { + name: "Status", + validation: { required: true }, + dataType: "string", + description: "Should this product be visible in the website", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + enumValues: { + private: "Private", + public: "Public" + } + }, + published: ({ values }) => buildProperty({ + name: "Published", + dataType: "boolean", + columnWidth: 100, + disabled: ( + values.status === "public" + ? false + : { + clearOnDisabled: true, + disabledMessage: "Status must be public in order to enable this the published flag" + } + ) + }), + related_products: { + dataType: "array", + name: "Related products", + description: "Reference to self", + of: { + dataType: "reference", + path: "products" + } + }, + main_image: buildProperty({ // The `buildProperty` method is a utility function used for type checking + name: "Image", + dataType: "string", + storage: { + storagePath: "images", + acceptedFiles: ["image/*"] + } + }), + tags: { + name: "Tags", + description: "Example of generic array", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string" + } + }, + description: { + name: "Description", + description: "This is the description of the product", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + dataType: "string", + columnWidth: 300 + }, + categories: { + name: "Categories", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string", + enumValues: { + electronics: "Electronics", + books: "Books", + furniture: "Furniture", + clothing: "Clothing", + food: "Food" + } + } + }, + publisher: { + name: "Publisher", + description: "This is an example of a map property", + dataType: "map", + properties: { + name: { + name: "Name", + dataType: "string" + }, + external_id: { + name: "External id", + dataType: "string" + } + } + }, + expires_on: { + name: "Expires on", + dataType: "date" + } + } +}); + +export default function App() { + + const myAuthenticator: Authenticator = useCallback(async ({ + user, + authController + }) => { + + if (user?.email?.includes("flanders")) { + throw Error("Stupid Flanders!"); + } + + console.log("Allowing access to", user?.email); + // This is an example of retrieving async data related to the user + // and storing it in the controller's extra field. + const sampleUserRoles = await Promise.resolve(["admin"]); + authController.setExtra(sampleUserRoles); + + return true; + }, []); + + return ; +} diff --git a/website/samples/samples_v2/nextjs.tsx b/website/samples/samples_v2/nextjs.tsx new file mode 100644 index 000000000..3f16c809c --- /dev/null +++ b/website/samples/samples_v2/nextjs.tsx @@ -0,0 +1,220 @@ +"use client"; + +import React, { useCallback } from "react"; + +import { User as FirebaseUser } from "firebase/auth"; +import { + Authenticator, + buildCollection, + buildProperty, + EntityReference, + FirebaseCMSApp +} from "firecms"; + +import "typeface-rubik"; +import "@fontsource/ibm-plex-mono"; + +// TODO: Replace with your config +const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; + +const locales = { + "en-US": "English (United States)", + "es-ES": "Spanish (Spain)", + "de-DE": "German" +}; + +type Product = { + name: string; + price: number; + status: string; + published: boolean; + related_products: EntityReference[]; + main_image: string; + tags: string[]; + description: string; + categories: string[]; + publisher: { + name: string; + external_id: string; + }, + expires_on: Date +} + +const localeCollection = buildCollection({ + path: "locale", + customId: locales, + name: "Locales", + singularName: "Locales", + properties: { + name: { + name: "Title", + validation: { required: true }, + dataType: "string" + }, + selectable: { + name: "Selectable", + description: "Is this locale selectable", + dataType: "boolean" + } + } +}); + +const productsCollection = buildCollection({ + name: "Products", + singularName: "Product", + path: "products", + permissions: ({ authController }) => ({ + edit: true, + create: true, + // we have created the roles object in the navigation builder + delete: false + }), + subcollections: [ + localeCollection + ], + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + name: "Price", + validation: { + required: true, + requiredMessage: "You must set a price between 0 and 1000", + min: 0, + max: 1000 + }, + description: "Price with range validation", + dataType: "number" + }, + status: { + name: "Status", + validation: { required: true }, + dataType: "string", + description: "Should this product be visible in the website", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + enumValues: { + private: "Private", + public: "Public" + } + }, + published: ({ values }) => buildProperty({ + name: "Published", + dataType: "boolean", + columnWidth: 100, + disabled: ( + values.status === "public" + ? false + : { + clearOnDisabled: true, + disabledMessage: "Status must be public in order to enable this the published flag" + } + ) + }), + related_products: { + dataType: "array", + name: "Related products", + description: "Reference to self", + of: { + dataType: "reference", + path: "products" + } + }, + main_image: buildProperty({ // The `buildProperty` method is a utility function used for type checking + name: "Image", + dataType: "string", + storage: { + storagePath: "images", + acceptedFiles: ["image/*"] + } + }), + tags: { + name: "Tags", + description: "Example of generic array", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string" + } + }, + description: { + name: "Description", + description: "This is the description of the product", + longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros.", + dataType: "string", + columnWidth: 300 + }, + categories: { + name: "Categories", + validation: { required: true }, + dataType: "array", + of: { + dataType: "string", + enumValues: { + electronics: "Electronics", + books: "Books", + furniture: "Furniture", + clothing: "Clothing", + food: "Food" + } + } + }, + publisher: { + name: "Publisher", + description: "This is an example of a map property", + dataType: "map", + properties: { + name: { + name: "Name", + dataType: "string" + }, + external_id: { + name: "External id", + dataType: "string" + } + } + }, + expires_on: { + name: "Expires on", + dataType: "date" + } + } +}); + +export default function CMS() { + + const myAuthenticator: Authenticator = useCallback(async ({ + user, + authController + }) => { + + if (user?.email?.includes("flanders")) { + throw Error("Stupid Flanders!"); + } + + console.log("Allowing access to", user?.email); + // This is an example of retrieving async data related to the user + // and storing it in the controller's extra field. + const sampleUserRoles = await Promise.resolve(["admin"]); + authController.setExtra(sampleUserRoles); + + return true; + }, []); + + return ; +} diff --git a/website/samples/samples_v2/recipes/blog/BlogEntryPreview.tsx b/website/samples/samples_v2/recipes/blog/BlogEntryPreview.tsx new file mode 100644 index 000000000..38818114b --- /dev/null +++ b/website/samples/samples_v2/recipes/blog/BlogEntryPreview.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useState } from "react"; +import { + Box, + CardActionArea, + CardContent, + CircularProgress, + Container, + Paper, + Typography +} from "@mui/material"; +import { + Entity, + EntityCustomViewParams, + EntityReference, + EntityValues, + ErrorView, + Markdown, + useDataSource, + useStorageSource +} from "firecms"; +import { productsCollection } from "./products_collection"; +import { BlogEntry, Product } from "./types"; + +/** + * This is a sample view used to render the content of a blog entry. + * It is bound to the data that is modified in the form. + */ +export function BlogEntryPreview({ modifiedValues }: EntityCustomViewParams) { + + const storage = useStorageSource(); + + const [headerUrl, setHeaderUrl] = useState(); + useEffect(() => { + if (modifiedValues?.header_image) { + storage.getDownloadURL(modifiedValues.header_image) + .then((res) => setHeaderUrl(res.url)); + } + }, [storage, modifiedValues?.header_image]); + + return ( + + + {headerUrl && {"Header"}} + + + {modifiedValues?.name && + {modifiedValues.name} + } + + {modifiedValues?.content && modifiedValues.content + .filter((e: any) => !!e) + .map( + (entry: any, index: number) => { + if (entry.type === "text") + return ; + if (entry.type === "images") + return ; + if (entry.type === "products") + return ; + return + } + )} + + + + + ); + +} + +export function Images({ storagePaths }: { storagePaths: string[] }) { + if (!Array.isArray(storagePaths)) + return <>; + return + {storagePaths.map((path, index) => + + + + )} + ; +} + +export function StorageImage({ storagePath }: { storagePath: string }) { + + const storage = useStorageSource(); + const [url, setUrl] = useState(); + useEffect(() => { + if (storagePath) { + storage.getDownloadURL(storagePath) + .then((res) => setUrl(res.url)); + } + }, [storage, storagePath]); + + if (!storagePath) + return <>; + + return ({"Generic"}); +} + +function Text({ markdownText }: { markdownText: string }) { + + if (!markdownText) + return <>; + + return + + + + ; +} + +function ProductGroupPreview({ references }: { references: EntityReference[] }) { + + const [products, setProducts] = useState[] | undefined>(); + const dataSource = useDataSource(); + + /** + * Fetch the products determined by the references, using the datasource + * and the products collection + */ + useEffect(() => { + if (references) { + Promise.all(references.map((ref) => dataSource.fetchEntity({ + path: ref.path, + entityId: ref.id, + collection: productsCollection + }))) + .then((results) => results.filter(r => !!r) as Entity[]) + .then((results) => setProducts(results)); + } + }, [references, dataSource]); + + if (!references) + return <>; + + if (!products) return ; + + return + {products.map((p, index) => }/>)} + ; +} + +export function ProductPreview({ productValues }: { productValues: EntityValues }) { + + if (!productValues) + return <>; + + return ( + + + + + + + + {productValues.name} + + + + {productValues.price} Euros + + + + + ); + +} diff --git a/website/samples/samples_v2/recipes/blog/blog_collection.tsx b/website/samples/samples_v2/recipes/blog/blog_collection.tsx new file mode 100644 index 000000000..6d284f67d --- /dev/null +++ b/website/samples/samples_v2/recipes/blog/blog_collection.tsx @@ -0,0 +1,94 @@ +import { buildCollection, buildProperty } from "firecms"; +import { BlogEntryPreview } from "./BlogEntryPreview"; +import { BlogEntry } from "./types"; + +export const blogCollection = buildCollection({ + name: "Blog entry", + path: "blog", + views: [{ + path: "preview", + name: "Preview", + Builder: BlogEntryPreview + }], + properties: { + name: buildProperty({ + name: "Name", + validation: { required: true }, + dataType: "string" + }), + header_image: buildProperty({ + name: "Header image", + dataType: "string", + storage: { + mediaType: "image", + storagePath: "images", + acceptedFiles: ["image/*"], + metadata: { + cacheControl: "max-age=1000000" + } + } + }), + content: buildProperty({ + name: "Content", + description: "Example of a complex array with multiple properties as children", + validation: { required: true }, + dataType: "array", + columnWidth: 400, + oneOf: { + typeField: "type", // you can ommit these `typeField` and `valueField` props to use the defaults + valueField: "value", + properties: { + images: buildProperty({ + name: "Images", + dataType: "array", + of: buildProperty({ + dataType: "string", + storage: { + mediaType: "image", + storagePath: "images", + acceptedFiles: ["image/*"], + metadata: { + cacheControl: "max-age=1000000" + } + } + }), + description: "This fields allows uploading multiple images at once and reordering" + }), + text: buildProperty({ + dataType: "string", + name: "Text", + markdown: true + }), + products: buildProperty({ + name: "Products", + dataType: "array", + of: { + dataType: "reference", + path: "products" // you need to define a valid collection in this path + } + }) + } + } + }), + status: buildProperty(({ values }) => ({ + name: "Status", + validation: { required: true }, + dataType: "string", + columnWidth: 140, + enumValues: { + published: { + id: "published", + label: "Published", + disabled: !values.header_image + }, + draft: "Draft" + }, + defaultValue: "draft" + })), + created_on: buildProperty({ + name: "Created on", + dataType: "date", + autoValue: "on_create" + }) + } +}) diff --git a/website/src/samples_v3/recipes/blog/products_collection.tsx b/website/samples/samples_v2/recipes/blog/products_collection.tsx similarity index 98% rename from website/src/samples_v3/recipes/blog/products_collection.tsx rename to website/samples/samples_v2/recipes/blog/products_collection.tsx index 75996db4e..ece9a1a36 100644 --- a/website/src/samples_v3/recipes/blog/products_collection.tsx +++ b/website/samples/samples_v2/recipes/blog/products_collection.tsx @@ -1,4 +1,4 @@ -import { buildCollection } from "@firecms/firebase"; +import { buildCollection } from "firecms"; import { Product } from "./types"; export const productsCollection = buildCollection({ diff --git a/website/src/samples_v3/recipes/blog/types.ts b/website/samples/samples_v2/recipes/blog/types.ts similarity index 93% rename from website/src/samples_v3/recipes/blog/types.ts rename to website/samples/samples_v2/recipes/blog/types.ts index bd3a34cec..c1e2079ed 100644 --- a/website/src/samples_v3/recipes/blog/types.ts +++ b/website/samples/samples_v2/recipes/blog/types.ts @@ -1,4 +1,4 @@ -import { EntityReference } from "@firecms/firebase"; +import { EntityReference } from "firecms"; export type Product = { name: string; diff --git a/website/samples/samples_v2/recipes/copy_entity/copy_button.tsx b/website/samples/samples_v2/recipes/copy_entity/copy_button.tsx new file mode 100644 index 000000000..c6b2582d2 --- /dev/null +++ b/website/samples/samples_v2/recipes/copy_entity/copy_button.tsx @@ -0,0 +1,63 @@ +import { + Entity, + EntityCollection, + useDataSource, + useReferenceDialog, + useSnackbarController +} from "firecms"; +import { Button } from "@mui/material"; +import { useCallback } from "react"; + +export type CopyEntityButtonProps = { + pathFrom: string; + pathTo: string; + collectionFrom: EntityCollection; + collectionTo: EntityCollection; +}; + +export function CopyEntityButton({ + pathFrom, + collectionFrom, + pathTo, + collectionTo + }: CopyEntityButtonProps) { + + // The datasource allows us to create new documents + const dataSource = useDataSource(); + + // We use a snackbar to indicate success + const snackbarController = useSnackbarController(); + + // We declare a callback function for the reference dialog that will + // create the new entity and show a snackbar when completed + const copyEntity = useCallback((entity: Entity | null) => { + if (entity) { + dataSource.saveEntity({ + path: pathTo, + values: entity.values, + entityId: entity.id, + collection: collectionTo, + status: "new" + }).then(() => { + snackbarController.open({ + type: "success", + message: "Copied entity " + entity.id + }); + }); + } + }, [collectionTo, dataSource, pathTo, snackbarController]); + + // This dialog is used to prompt the selected collection + const referenceDialog = useReferenceDialog({ + path: pathFrom, + collection: collectionFrom, + multiselect: false, + onSingleEntitySelected: copyEntity + }); + + return ( + + ); +} diff --git a/website/src/samples_v3/recipes/copy_entity/copy_button_use.tsx b/website/samples/samples_v2/recipes/copy_entity/copy_button_use.tsx similarity index 87% rename from website/src/samples_v3/recipes/copy_entity/copy_button_use.tsx rename to website/samples/samples_v2/recipes/copy_entity/copy_button_use.tsx index 8e29d8830..bf2213282 100644 --- a/website/src/samples_v3/recipes/copy_entity/copy_button_use.tsx +++ b/website/samples/samples_v2/recipes/copy_entity/copy_button_use.tsx @@ -1,4 +1,4 @@ -import { buildCollection, CollectionActionsProps } from "@firecms/firebase"; +import { buildCollection, CollectionActionsProps } from "firecms"; import { CopyEntityButton } from "./copy_button"; import { Product, diff --git a/website/samples/samples_v2/recipes/copy_entity/full.tsx b/website/samples/samples_v2/recipes/copy_entity/full.tsx new file mode 100644 index 000000000..de2c668c4 --- /dev/null +++ b/website/samples/samples_v2/recipes/copy_entity/full.tsx @@ -0,0 +1,109 @@ +import { + buildCollection, + buildProperties, + CollectionActionsProps, + Entity, + EntityCollection, + useDataSource, + useReferenceDialog, + useSnackbarController +} from "firecms"; +import { Button } from "@mui/material"; +import { useCallback } from "react"; + +type Product = { + name: string; + price: number; +} + +type CopyEntityButtonProps = { + pathFrom: string; + pathTo: string; + collectionFrom: EntityCollection; + collectionTo: EntityCollection; +}; + +function CopyEntityButton({ + pathFrom, + collectionFrom, + pathTo, + collectionTo + }: CopyEntityButtonProps) { + + // The datasource allows us to create new documents + const dataSource = useDataSource(); + + // We use a snackbar to indicate success + const snackbarController = useSnackbarController(); + + // We declare a callback function for the reference dialog that will + // create the new entity and show a snackbar when completed + const copyEntity = useCallback((entity: Entity | null) => { + if (entity) { + dataSource.saveEntity({ + path: pathTo, + values: entity.values, + entityId: entity.id, + collection: collectionTo, + status: "new" + }).then(() => { + snackbarController.open({ + type: "success", + message: "Copied entity " + entity.id + }); + }); + } + }, [collectionTo, dataSource, pathTo, snackbarController]); + + // This dialog is used to prompt the selected collection + const referenceDialog = useReferenceDialog({ + path: pathFrom, + collection: collectionFrom, + multiselect: false, + onSingleEntitySelected: copyEntity + }); + + return ( + + ); +} + +// Common properties of our target and source collections +const properties = buildProperties({ + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + name: "Price", + validation: { + required: true, + min: 0 + }, + dataType: "number" + } +}); + +// Source collection +export const productsCollection = buildCollection({ + name: "Products", + path: "products", + properties +}); + +// Target collection +export const productsCollectionCopy = buildCollection({ + name: "Products copy target", + path: "products_copied", + properties, + Actions: ({ path, collection }: CollectionActionsProps) => + +}); diff --git a/website/src/samples_v3/recipes/copy_entity/simple_product_collection.tsx b/website/samples/samples_v2/recipes/copy_entity/simple_product_collection.tsx similarity index 91% rename from website/src/samples_v3/recipes/copy_entity/simple_product_collection.tsx rename to website/samples/samples_v2/recipes/copy_entity/simple_product_collection.tsx index bee4d0740..d30d1af83 100644 --- a/website/src/samples_v3/recipes/copy_entity/simple_product_collection.tsx +++ b/website/samples/samples_v2/recipes/copy_entity/simple_product_collection.tsx @@ -1,4 +1,4 @@ -import { buildCollection, buildProperties } from "@firecms/firebase"; +import { buildCollection, buildProperties } from "firecms"; export type Product = { name: string; diff --git a/website/samples/samples_v2/recipes/custom_datasource/custom_datasource.tsx b/website/samples/samples_v2/recipes/custom_datasource/custom_datasource.tsx new file mode 100644 index 000000000..88d65691c --- /dev/null +++ b/website/samples/samples_v2/recipes/custom_datasource/custom_datasource.tsx @@ -0,0 +1,71 @@ +import { + CMSType, + DataSource, + DeleteEntityProps, + Entity, + EntityCollection, + FetchCollectionProps, + FetchEntityProps, + ResolvedProperty, + SaveEntityProps, + useFirestoreDataSource +} from "firecms"; +import { FirebaseApp } from "firebase/app"; + +type CustomDataSourceProps = { firebaseApp?: FirebaseApp }; + +/** + * This is an example of a custom data source. + * It is a React Hook that returns a {@link DataSource} object. + * @param firebaseApp + */ +export function useCustomDatasource({ firebaseApp }: CustomDataSourceProps): DataSource { + + const firestoreDataSource = useFirestoreDataSource({ + firebaseApp + }); + + return { + fetchCollection(props: FetchCollectionProps): Promise[]> { + if (props.path === "your_path_custom") { + // make your custom http call and return your Entities + } + return firestoreDataSource.fetchCollection(props); + }, + fetchEntity(props: FetchEntityProps): Promise | undefined> { + if (props.path === "your_path_custom") { + // make your custom http call and return your Entities + } + return firestoreDataSource.fetchEntity(props); + }, + saveEntity(props: SaveEntityProps): Promise> { + if (props.path === "your_path_custom") { + // make your custom http call and return your Entities + } + return firestoreDataSource.saveEntity(props); + }, + deleteEntity(props: DeleteEntityProps): Promise { + return firestoreDataSource.deleteEntity(props); + }, + checkUniqueField(path: string, name: string, value: any, property: ResolvedProperty, entityId?: string): Promise { + return firestoreDataSource.checkUniqueField(path, name, value, property, entityId); + }, + generateEntityId(path: string) { + return firestoreDataSource.generateEntityId(path); + }, + countEntities(path: { + path: string, + collection: EntityCollection, + }): Promise { + return firestoreDataSource.countEntities(path); + } + } +} diff --git a/website/src/samples_v3/recipes/document_as_subcollection/collection_builder.tsx b/website/samples/samples_v2/recipes/document_as_subcollection/collection_builder.tsx similarity index 89% rename from website/src/samples_v3/recipes/document_as_subcollection/collection_builder.tsx rename to website/samples/samples_v2/recipes/document_as_subcollection/collection_builder.tsx index f721e973c..bbc7bc26d 100644 --- a/website/src/samples_v3/recipes/document_as_subcollection/collection_builder.tsx +++ b/website/samples/samples_v2/recipes/document_as_subcollection/collection_builder.tsx @@ -1,4 +1,4 @@ -import { buildCollection, EntityCollectionsBuilder } from "@firecms/firebase"; +import { buildCollection, EntityCollectionsBuilder } from "firecms"; import { Unit, unitsCollection } from "./unit_collection"; const collectionBuilder: EntityCollectionsBuilder = async ({ dataSource }) => { diff --git a/website/samples/samples_v2/recipes/document_as_subcollection/full.tsx b/website/samples/samples_v2/recipes/document_as_subcollection/full.tsx new file mode 100644 index 000000000..a5f04696c --- /dev/null +++ b/website/samples/samples_v2/recipes/document_as_subcollection/full.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import { + buildCollection, + EntityCollectionsBuilder, + FirebaseCMSApp +} from "firecms"; + +import "typeface-rubik"; +import "@fontsource/ibm-plex-mono"; + +// TODO: Replace with your config +const firebaseConfig = { + apiKey: "", + authDomain: "", + projectId: "", + storageBucket: "", + messagingSenderId: "", + appId: "" +}; + +type Unit = { + name: string; + description: string; +} + +const unitsCollection = buildCollection({ + name: "Units", + singularName: "Unit", + group: "Main", + path: "units", + customId: true, + icon: "LocalLibrary", + callbacks: { + onSaveSuccess: ({ context }) => { + context.navigation.refreshNavigation(); + }, + onDelete: ({ context }) => { + context.navigation.refreshNavigation(); + } + }, + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + description: { + name: "Description", + validation: { required: true }, + dataType: "string", + multiline: true + } + } +}); + +export default function App() { + + const collectionBuilder: EntityCollectionsBuilder = async ({ dataSource }) => { + const units = await dataSource.fetchCollection({ + path: "units", + collection: unitsCollection + }); + const lessonCollections = units.map(unit => buildCollection({ + name: unit.values.name, + path: `units/${unit.id}/lessons`, + description: unit.values.description, + group: "Units", + properties: { + name: { + name: "Name", + dataType: "string" + } + } + })); + + return [ + unitsCollection, + ...lessonCollections + ] + }; + + return ; +} diff --git a/website/src/samples_v3/recipes/document_as_subcollection/unit_collection.tsx b/website/samples/samples_v2/recipes/document_as_subcollection/unit_collection.tsx similarity index 93% rename from website/src/samples_v3/recipes/document_as_subcollection/unit_collection.tsx rename to website/samples/samples_v2/recipes/document_as_subcollection/unit_collection.tsx index ace8250bf..d76c35843 100644 --- a/website/src/samples_v3/recipes/document_as_subcollection/unit_collection.tsx +++ b/website/samples/samples_v2/recipes/document_as_subcollection/unit_collection.tsx @@ -1,4 +1,4 @@ -import { buildCollection } from "@firecms/firebase"; +import { buildCollection } from "firecms"; export type Unit = { name: string; diff --git a/website/src/samples_v3/custom_cms_view/CustomViewSampleApp.tsx b/website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx similarity index 96% rename from website/src/samples_v3/custom_cms_view/CustomViewSampleApp.tsx rename to website/samples/samples_v3/custom_cms_view/CustomViewSampleApp.tsx index 3eafbcffc..cee0094d7 100644 --- a/website/src/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/firebase"; +import { buildCollection, CMSView, FireCMS3App, FireCMSAppConfig } from "firecms"; import { ExampleCMSView } from "./ExampleCMSView"; const projectId = "YOUR_PROJECT_ID"; diff --git a/website/src/samples_v3/custom_cms_view/ExampleCMSView.tsx b/website/samples/samples_v3/custom_cms_view/ExampleCMSView.tsx similarity index 99% rename from website/src/samples_v3/custom_cms_view/ExampleCMSView.tsx rename to website/samples/samples_v3/custom_cms_view/ExampleCMSView.tsx index 2705fd2f0..c5b939e4f 100644 --- a/website/src/samples_v3/custom_cms_view/ExampleCMSView.tsx +++ b/website/samples/samples_v3/custom_cms_view/ExampleCMSView.tsx @@ -14,7 +14,7 @@ import { useSelectionController, useSideEntityController, useSnackbarController -} from "@firecms/firebase"; +} from "firecms"; const usersCollection = buildCollection({ path: "users", diff --git a/website/src/samples_v3/nextjs.tsx b/website/samples/samples_v3/nextjs.tsx similarity index 99% rename from website/src/samples_v3/nextjs.tsx rename to website/samples/samples_v3/nextjs.tsx index b74522d8e..4f071df57 100644 --- a/website/src/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/firebase"; +import { buildCollection, buildProperty, EntityReference, FireCMS3App, } from "firecms"; import "typeface-rubik"; import "@fontsource/ibm-plex-mono"; diff --git a/website/src/samples_v3/recipes/blog/BlogEntryPreview.tsx b/website/samples/samples_v3/recipes/blog/BlogEntryPreview.tsx similarity index 99% rename from website/src/samples_v3/recipes/blog/BlogEntryPreview.tsx rename to website/samples/samples_v3/recipes/blog/BlogEntryPreview.tsx index 2c5105252..88f7d0ad2 100644 --- a/website/src/samples_v3/recipes/blog/BlogEntryPreview.tsx +++ b/website/samples/samples_v3/recipes/blog/BlogEntryPreview.tsx @@ -12,7 +12,7 @@ import { Typography, useDataSource, useStorageSource -} from "@firecms/firebase"; +} from "firecms"; import { productsCollection } from "./products_collection"; import { BlogEntry, Product } from "./types"; diff --git a/website/src/samples_v3/recipes/blog/blog_collection.tsx b/website/samples/samples_v3/recipes/blog/blog_collection.tsx similarity index 97% rename from website/src/samples_v3/recipes/blog/blog_collection.tsx rename to website/samples/samples_v3/recipes/blog/blog_collection.tsx index b85416921..0c1062c20 100644 --- a/website/src/samples_v3/recipes/blog/blog_collection.tsx +++ b/website/samples/samples_v3/recipes/blog/blog_collection.tsx @@ -1,4 +1,4 @@ -import { buildCollection, buildProperty } from "@firecms/firebase"; +import { buildCollection, buildProperty } from "firecms"; import { BlogEntryPreview } from "./BlogEntryPreview"; import { BlogEntry } from "./types"; diff --git a/website/samples/samples_v3/recipes/blog/products_collection.tsx b/website/samples/samples_v3/recipes/blog/products_collection.tsx new file mode 100644 index 000000000..ece9a1a36 --- /dev/null +++ b/website/samples/samples_v3/recipes/blog/products_collection.tsx @@ -0,0 +1,136 @@ +import { buildCollection } from "firecms"; +import { Product } from "./types"; + +export const productsCollection = buildCollection({ + path: "products", + alias: "ppp", + name: "Products", + singularName: "Product", + group: "Main", + icon: "ShoppingCart", + description: "List of the products currently sold in our shop", + textSearchEnabled: true, + properties: { + name: { + dataType: "string", + name: "Name", + description: "Name of this product", + validation: { + required: true + } + }, + main_image: { + dataType: "string", + name: "Image", + storage: { + storagePath: "images", + acceptedFiles: ["image/*"], + maxSize: 1024 * 1024, + metadata: { + cacheControl: "max-age=1000000" + } + }, + description: "Upload field for images", + validation: { + required: true + } + }, + available: { + dataType: "boolean", + name: "Available", + columnWidth: 100, + description: "Is this product available in the website" + }, + price: ({ values }) => ({ + dataType: "number", + name: "Price", + validation: { + requiredMessage: "You must set a price between 0 and 1000", + min: 0, + max: 1000 + }, + disabled: !values.available && { + clearOnDisabled: true, + disabledMessage: "You can only set the price on available items" + }, + description: "Price with range validation" + }), + public: { + dataType: "boolean", + name: "Public", + description: "Should this product be visible in the website" + // longDescription: "Example of a long description hidden under a tooltip. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quis bibendum turpis. Sed scelerisque ligula nec nisi pellentesque, eget viverra lorem facilisis. Praesent a lectus ac ipsum tincidunt posuere vitae non risus. In eu feugiat massa. Sed eu est non velit facilisis facilisis vitae eget ante. Nunc ut malesuada erat. Nullam sagittis bibendum porta. Maecenas vitae interdum sapien, ut aliquet risus. Donec aliquet, turpis finibus aliquet bibendum, tellus dui porttitor quam, quis pellentesque tellus libero non urna. Vestibulum maximus pharetra congue. Suspendisse aliquam congue quam, sed bibendum turpis. Aliquam eu enim ligula. Nam vel magna ut urna cursus sagittis. Suspendisse a nisi ac justo ornare tempor vel eu eros." + }, + brand: { + dataType: "string", + name: "Brand", + validation: { + required: true + } + }, + description: { + dataType: "string", + name: "Description", + description: "Example of a markdown field", + markdown: true + }, + amazon_link: { + dataType: "string", + name: "Amazon link", + url: true + }, + images: { + dataType: "array", + name: "Images", + hideFromCollection: true, + of: { + dataType: "string", + storage: { + storagePath: "images", + acceptedFiles: ["image/*"], + metadata: { + cacheControl: "max-age=1000000" + } + } + }, + description: "This fields allows uploading multiple images at once" + }, + related_products: { + dataType: "array", + name: "Related products", + description: "Reference to self", + of: { + dataType: "reference", + path: "ppp" + } + }, + publisher: { + name: "Publisher", + description: "This is an example of a map property", + dataType: "map", + properties: { + name: { + name: "Name", + dataType: "string" + }, + external_id: { + name: "External id", + dataType: "string" + } + }, + expanded: true + }, + uppercase_name: { + name: "Uppercase Name", + dataType: "string", + readOnly: true, + description: "This field gets updated with a preSave callback" + }, + added_on: { + dataType: "date", + name: "Added on", + autoValue: "on_create" + } + } + +}); diff --git a/website/samples/samples_v3/recipes/blog/types.ts b/website/samples/samples_v3/recipes/blog/types.ts new file mode 100644 index 000000000..c1e2079ed --- /dev/null +++ b/website/samples/samples_v3/recipes/blog/types.ts @@ -0,0 +1,43 @@ +import { EntityReference } from "firecms"; + +export type Product = { + name: string; + main_image: string; + available: boolean; + price: number; + public: boolean; + brand: string; + description: string; + amazon_link: string; + images: string[]; + related_products: EntityReference[]; + publisher: { + name: string; + external_id: string; + }, + uppercase_name: string, + added_on: Date; +} + +export type BlogEntry = { + name: string; + header_image: string; + content: (BlogEntryImages | BlogEntryText | BlogEntryProducts)[]; + status: string; + created_on: Date +} + +export type BlogEntryImages = { + type: "images"; + value: string[]; +} + +export type BlogEntryText = { + type: "text"; + value: string; +} + +export type BlogEntryProducts = { + type: "products"; + value: Product[]; +} diff --git a/website/src/samples_v3/recipes/copy_entity/copy_button.tsx b/website/samples/samples_v3/recipes/copy_entity/copy_button.tsx similarity index 98% rename from website/src/samples_v3/recipes/copy_entity/copy_button.tsx rename to website/samples/samples_v3/recipes/copy_entity/copy_button.tsx index 286dd6f2f..f4dda5f9c 100644 --- a/website/src/samples_v3/recipes/copy_entity/copy_button.tsx +++ b/website/samples/samples_v3/recipes/copy_entity/copy_button.tsx @@ -5,7 +5,7 @@ import { useDataSource, useReferenceDialog, useSnackbarController -} from "@firecms/firebase"; +} from "firecms"; import { useCallback } from "react"; export type CopyEntityButtonProps = { diff --git a/website/samples/samples_v3/recipes/copy_entity/copy_button_use.tsx b/website/samples/samples_v3/recipes/copy_entity/copy_button_use.tsx new file mode 100644 index 000000000..bf2213282 --- /dev/null +++ b/website/samples/samples_v3/recipes/copy_entity/copy_button_use.tsx @@ -0,0 +1,20 @@ +import { buildCollection, CollectionActionsProps } from "firecms"; +import { CopyEntityButton } from "./copy_button"; +import { + Product, + productsCollection, + properties +} from "./simple_product_collection"; + +export const productsCollectionCopy = buildCollection({ + name: "Products copy target", + path: "products_copied", + properties, + Actions: ({ path, collection }: CollectionActionsProps) => + +}); diff --git a/website/src/samples_v3/recipes/copy_entity/full.tsx b/website/samples/samples_v3/recipes/copy_entity/full.tsx similarity index 99% rename from website/src/samples_v3/recipes/copy_entity/full.tsx rename to website/samples/samples_v3/recipes/copy_entity/full.tsx index 044079f19..fddb30a4a 100644 --- a/website/src/samples_v3/recipes/copy_entity/full.tsx +++ b/website/samples/samples_v3/recipes/copy_entity/full.tsx @@ -8,7 +8,7 @@ import { useDataSource, useReferenceDialog, useSnackbarController -} from "@firecms/firebase"; +} from "firecms"; import { useCallback } from "react"; type Product = { diff --git a/website/samples/samples_v3/recipes/copy_entity/simple_product_collection.tsx b/website/samples/samples_v3/recipes/copy_entity/simple_product_collection.tsx new file mode 100644 index 000000000..d30d1af83 --- /dev/null +++ b/website/samples/samples_v3/recipes/copy_entity/simple_product_collection.tsx @@ -0,0 +1,37 @@ +import { buildCollection, buildProperties } from "firecms"; + +export type Product = { + name: string; + price: number; +} + +// Common properties of our target and source collections +export const properties = buildProperties({ + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + price: { + name: "Price", + validation: { + required: true, + min: 0 + }, + dataType: "number" + } +}); + +// Source collection +export const productsCollection = buildCollection({ + name: "Products", + path: "products", + properties +}); + +// Target collection +export const productsCollectionCopy = buildCollection({ + name: "Products copy target", + path: "products_copied", + properties +}); diff --git a/website/samples/samples_v3/recipes/document_as_subcollection/collection_builder.tsx b/website/samples/samples_v3/recipes/document_as_subcollection/collection_builder.tsx new file mode 100644 index 000000000..bbc7bc26d --- /dev/null +++ b/website/samples/samples_v3/recipes/document_as_subcollection/collection_builder.tsx @@ -0,0 +1,26 @@ +import { buildCollection, EntityCollectionsBuilder } from "firecms"; +import { Unit, unitsCollection } from "./unit_collection"; + +const collectionBuilder: EntityCollectionsBuilder = async ({ dataSource }) => { + const units = await dataSource.fetchCollection({ + path: "units", + collection: unitsCollection + }); + const lessonCollections = units.map(unit => buildCollection({ + name: unit.values.name, + path: `units/${unit.id}/lessons`, + description: unit.values.description, + group: "Units", + properties: { + name: { + name: "Name", + dataType: "string" + } + } + })); + + return [ + unitsCollection, + ...lessonCollections + ] +}; diff --git a/website/src/samples_v3/recipes/document_as_subcollection/full.tsx b/website/samples/samples_v3/recipes/document_as_subcollection/full.tsx similarity index 95% rename from website/src/samples_v3/recipes/document_as_subcollection/full.tsx rename to website/samples/samples_v3/recipes/document_as_subcollection/full.tsx index bff83a2ca..f8af46852 100644 --- a/website/src/samples_v3/recipes/document_as_subcollection/full.tsx +++ b/website/samples/samples_v3/recipes/document_as_subcollection/full.tsx @@ -1,4 +1,4 @@ -import { buildCollection, FireCMSAppConfig } from "@firecms/firebase"; +import { buildCollection, FireCMSAppConfig } from "firecms"; type Unit = { name: string; diff --git a/website/samples/samples_v3/recipes/document_as_subcollection/unit_collection.tsx b/website/samples/samples_v3/recipes/document_as_subcollection/unit_collection.tsx new file mode 100644 index 000000000..d76c35843 --- /dev/null +++ b/website/samples/samples_v3/recipes/document_as_subcollection/unit_collection.tsx @@ -0,0 +1,36 @@ +import { buildCollection } from "firecms"; + +export type Unit = { + name: string; + description: string; +} + +export const unitsCollection = buildCollection({ + name: "Units", + singularName: "Unit", + group: "Main", + path: "units", + customId: true, + icon: "LocalLibrary", + callbacks: { + onSaveSuccess: ({ context }) => { + context.navigation.refreshNavigation(); + }, + onDelete: ({ context }) => { + context.navigation.refreshNavigation(); + } + }, + properties: { + name: { + name: "Name", + validation: { required: true }, + dataType: "string" + }, + description: { + name: "Description", + validation: { required: true }, + dataType: "string", + multiline: true + } + } +}); diff --git a/website/sidebars.js b/website/sidebars.js index 5319241b7..6b536add4 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -95,17 +95,17 @@ module.exports = { ] }, "changelog", - { - "type":"category", - "label":"API reference", - "collapsed":true, - "items":[ - { - "type":"autogenerated", - "dirName":"api" - } - ] - }, + // { + // "type":"category", + // "label":"API reference", + // "collapsed":true, + // "items":[ + // { + // "type":"autogenerated", + // "dirName":"api" + // } + // ] + // }, ] } diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 9e5712872..e4c02dc84 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -306,6 +306,12 @@ html[data-theme=dark] .header-github-link:before { .footer { z-index: 0; } +.theme-doc-sidebar-container { + background-color: var(--ifm-background-color); +} +html[data-theme=dark] .theme-doc-sidebar-container { + background-color: var(--ifm-background-color); +} html[data-theme=dark] .header-github-link:before { background: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") no-repeat; diff --git a/website/src/partials/UsageExamples.tsx b/website/src/partials/UsageExamples.tsx index 7a3a67721..6e7cacd23 100644 --- a/website/src/partials/UsageExamples.tsx +++ b/website/src/partials/UsageExamples.tsx @@ -8,11 +8,11 @@ import inlineEditing from "@site/static/img/inline_table_editing.mp4"; import customFieldDarkVideo from "@site/static/img/custom_fields_dark.mp4"; // @ts-ignore -import MMApp from "@site/static/img/mm_app.png"; +import MMApp from "@site/static/img/mm_app.webp"; // @ts-ignore import editorWhite from "@site/static/img/editor_white.png"; // @ts-ignore -import overlay from "@site/static/img/overlay.png"; +import overlay from "@site/static/img/overlay.webp"; import { BrowserFrame } from "./BrowserFrame"; import { PhoneFrame } from "./PhoneFrame"; @@ -43,8 +43,6 @@ export function UsageExamples() { const currentTop = ref.current?.getBoundingClientRect().top ?? 0; const parallaxOffset = easeInOut(Math.max(0, Math.min(1, (300 + currentTop) / offsetHeight))) * 2 - 1; - console.log({ parallaxOffset, ref, currentTop, offsetHeight, scroll }); - return
    diff --git a/website/src/partials/general/EnterpriseTeaser.tsx b/website/src/partials/general/EnterpriseTeaser.tsx index 9e8165595..6b8ee016d 100644 --- a/website/src/partials/general/EnterpriseTeaser.tsx +++ b/website/src/partials/general/EnterpriseTeaser.tsx @@ -22,7 +22,7 @@ export function EnterpriseTeaser() {
    - Learn more + About the enterprise plan diff --git a/website/src/partials/home/OpenAITeaser.tsx b/website/src/partials/home/OpenAITeaser.tsx index d13f4ca5f..92347ef02 100644 --- a/website/src/partials/home/OpenAITeaser.tsx +++ b/website/src/partials/home/OpenAITeaser.tsx @@ -62,7 +62,7 @@ function OpenAITeaser() { className="mt-5 sm:mt-8"> - Learn more + About this integration diff --git a/website/src/partials/pricing/CLIInstructions.tsx b/website/src/partials/pricing/CLIInstructions.tsx index fcbf99187..7e2f7f25a 100644 --- a/website/src/partials/pricing/CLIInstructions.tsx +++ b/website/src/partials/pricing/CLIInstructions.tsx @@ -5,7 +5,10 @@ import useBaseUrl from "@docusaurus/useBaseUrl"; export function CLIInstructions() { - return + return

    Start hacking today diff --git a/website/src/partials/pricing/FireCMSCloudVersions.tsx b/website/src/partials/pricing/FireCMSCloudVersions.tsx index 1e09c40a8..7f89662c9 100644 --- a/website/src/partials/pricing/FireCMSCloudVersions.tsx +++ b/website/src/partials/pricing/FireCMSCloudVersions.tsx @@ -87,6 +87,7 @@ export function FireCMSCloudVersions() {
  • Custom authentication and access control
  • SAML SSO
  • Custom domain
  • +
  • Full CMS components customization
  • Priority support
  • Roadmap prioritization
  • @@ -106,9 +107,9 @@ export function FireCMSCloudVersions() { ); return
    + className={clsx(defaultBorderMixin, "flex flex-col items-center justify-center text-lg border-0 border-t bg-white dark:bg-gray-800")}> -
    +

    Full no-code/low-code solution @@ -124,7 +125,7 @@ export function FireCMSCloudVersions() {
    {freeTier} {plusTier} diff --git a/website/src/partials/pricing/VersionsComparison.tsx b/website/src/partials/pricing/VersionsComparison.tsx index bccf11b27..7c89a521b 100644 --- a/website/src/partials/pricing/VersionsComparison.tsx +++ b/website/src/partials/pricing/VersionsComparison.tsx @@ -110,30 +110,42 @@ const data = [{ cloud: "No", cloudPlus: "Yes", cloudPro: "Yes" + }, { + feature: "Logo customisation", + selfHosted: "Yes", + cloud: "No", + cloudPlus: "Yes", + cloudPro: "Yes" }, { feature: "Self-hosted version", selfHosted: "Yes", cloud: "No", - cloudPlus: "WIP", - cloudPro: "WIP" + cloudPlus: "No", + cloudPro: "Yes" }, { feature: "Custom login screen", selfHosted: "Yes", cloud: "No", - cloudPlus: "WIP", - cloudPro: "WIP" + cloudPlus: "No", + cloudPro: "Yes" }, { - feature: "Theme and logo customisation", + feature: "Full theme customization", selfHosted: "Yes", cloud: "No", - cloudPlus: "WIP", - cloudPro: "WIP" + cloudPlus: "No", + cloudPro: "Yes" + }, { + feature: "CMS components customization", + selfHosted: "Yes", + cloud: "No", + cloudPlus: "No", + cloudPro: "Yes" }, { feature: "Text search", selfHosted: "Dev managed", cloud: "No", - cloudPlus: "No", - cloudPro: "WIP" + cloudPlus: "Dev managed", + cloudPro: "Dev managed" }, { feature: "Custom domain", selfHosted: "No", @@ -204,15 +216,15 @@ export function VersionsComparison() { className="border-none rounded-lg md:px-6 py-6 invisible"> + className="border-none rounded-lg text-base md:px-4 md:py-4 text-center w-[90px]"> Free + className="border-none rounded-lg text-base md:px-4 md:py-4 text-center w-[90px]"> Plus + className="border-none rounded-lg text-base md:px-4 md:py-4 text-center w-[90px]"> Pro {/* + className="bg-gray-50 mx-2 dark:bg-gray-900 border-none rounded-lg px-6 py-2 text-gray-800 dark:text-gray-300 font-bold"> {row.feature} @@ -239,7 +251,7 @@ export function VersionsComparison() { {getFeatureComponent(row.cloudPro)} - {/**/} + {/**/} {/* {getFeatureComponent(row.selfHosted)}*/} {/**/} @@ -265,7 +277,7 @@ export function VersionsComparison() { {/**/} {/* Self-hosted docs*/} diff --git a/website/static/img/blog_example.webp b/website/static/img/blog_example.webp new file mode 100644 index 000000000..7c381c836 Binary files /dev/null and b/website/static/img/blog_example.webp differ diff --git a/website/static/img/form_fields.png b/website/static/img/form_fields.png deleted file mode 100644 index d5703a687..000000000 Binary files a/website/static/img/form_fields.png and /dev/null differ diff --git a/website/static/img/mm_app.webp b/website/static/img/mm_app.webp new file mode 100644 index 000000000..16c22a29f Binary files /dev/null and b/website/static/img/mm_app.webp differ diff --git a/website/static/img/overlay.webp b/website/static/img/overlay.webp new file mode 100644 index 000000000..9c1c867b0 Binary files /dev/null and b/website/static/img/overlay.webp differ diff --git a/website/static/img/post_editing.webp b/website/static/img/post_editing.webp new file mode 100644 index 000000000..7b8ee627d Binary files /dev/null and b/website/static/img/post_editing.webp differ diff --git a/website/static/img/product_selection.webp b/website/static/img/product_selection.webp new file mode 100644 index 000000000..d885bf144 Binary files /dev/null and b/website/static/img/product_selection.webp differ diff --git a/website/typedoc.json b/website/typedoc.json index b7984847f..f08d6be83 100644 --- a/website/typedoc.json +++ b/website/typedoc.json @@ -5,7 +5,7 @@ "readme": "docs/api_index.md", "name": "FireCMS API", "categorizeByGroup": true, - "entryPoints": ["../packages/firebase_firecms/src/index.ts"], + "entryPoints": ["../packages/firecms/src/index.ts"], "hideGenerator": true, "hideParameterTypesInTitle": true, "excludeExternals": true, diff --git a/website/versioned_docs/version-2.0.0/custom_cms_app.mdx b/website/versioned_docs/version-2.0.0/custom_cms_app.mdx index 7212f0da8..5b80bc0c5 100644 --- a/website/versioned_docs/version-2.0.0/custom_cms_app.mdx +++ b/website/versioned_docs/version-2.0.0/custom_cms_app.mdx @@ -57,7 +57,7 @@ You will also be responsible for initialising your MUI5 theme and your react-rou import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../../examples/example/src/docs/CustomCMSApp"; + from "!!raw-loader!../../samples/samples_v2/CustomCMSApp"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/legacy_quickstart.mdx b/website/versioned_docs/version-2.0.0/legacy_quickstart.mdx index 9ca5bca53..155c0db31 100644 --- a/website/versioned_docs/version-2.0.0/legacy_quickstart.mdx +++ b/website/versioned_docs/version-2.0.0/legacy_quickstart.mdx @@ -48,7 +48,7 @@ If you don't know where to find the Firebase app config, check the import CodeBlock from "@theme/CodeBlock"; -import MyComponentSource from "!!raw-loader!../../../examples/example/src/docs/SimpleApp"; +import MyComponentSource from "!!raw-loader!../../samples/samples_v2/SimpleApp"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/navigation/custom_top_level_views.mdx b/website/versioned_docs/version-2.0.0/navigation/custom_top_level_views.mdx index 2b9f96cf3..499040baf 100644 --- a/website/versioned_docs/version-2.0.0/navigation/custom_top_level_views.mdx +++ b/website/versioned_docs/version-2.0.0/navigation/custom_top_level_views.mdx @@ -96,7 +96,7 @@ some hooks provided by the CMS: import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../../../examples/example/src/SampleApp/ExampleCMSView"; + from "!!raw-loader!../../../samples/samples_v2/ExampleCMSView"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/nextjs.mdx b/website/versioned_docs/version-2.0.0/nextjs.mdx index dd999e0f0..fe255fcbf 100644 --- a/website/versioned_docs/version-2.0.0/nextjs.mdx +++ b/website/versioned_docs/version-2.0.0/nextjs.mdx @@ -57,7 +57,7 @@ are running it. In this case, we are running it in `/cms`. ::: import CodeBlock from "@theme/CodeBlock"; -import MyComponentSource from "!!raw-loader!../../../examples/example/src/docs/nextjs.tsx"; +import MyComponentSource from "!!raw-loader!../../samples/samples_v2/nextjs.tsx"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/recipes/blog.mdx b/website/versioned_docs/version-2.0.0/recipes/blog.mdx index b026c30fe..82594957c 100644 --- a/website/versioned_docs/version-2.0.0/recipes/blog.mdx +++ b/website/versioned_docs/version-2.0.0/recipes/blog.mdx @@ -295,6 +295,6 @@ we get the following code for the blog collection: import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../../../examples/example/src/docs/recipes/blog/blog_collection"; + from "!!raw-loader!../../../samples/samples_v2/recipes/blog/blog_collection"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/recipes/copy_entity.mdx b/website/versioned_docs/version-2.0.0/recipes/copy_entity.mdx index 704303f0f..d1d91a949 100644 --- a/website/versioned_docs/version-2.0.0/recipes/copy_entity.mdx +++ b/website/versioned_docs/version-2.0.0/recipes/copy_entity.mdx @@ -28,13 +28,13 @@ that we will be copying from and to: import CodeBlock from "@theme/CodeBlock"; import MyComponentSource - from "!!raw-loader!../../../../examples/example/src/docs/recipes/copy_entity/simple_product_collection"; + from "!!raw-loader!../../../samples/samples_v2/recipes/copy_entity/simple_product_collection"; import CopyButton - from "!!raw-loader!../../../../examples/example/src/docs/recipes/copy_entity/copy_button"; + from "!!raw-loader!../../../samples/samples_v2/recipes/copy_entity/copy_button"; import CopyButtonUse - from "!!raw-loader!../../../../examples/example/src/docs/recipes/copy_entity/copy_button_use"; + from "!!raw-loader!../../../samples/samples_v2/recipes/copy_entity/copy_button_use"; import FullCode - from "!!raw-loader!../../../../examples/example/src/docs/recipes/copy_entity/full"; + from "!!raw-loader!../../../samples/samples_v2/recipes/copy_entity/full"; {MyComponentSource} diff --git a/website/versioned_docs/version-2.0.0/recipes/custom_datasource.mdx b/website/versioned_docs/version-2.0.0/recipes/custom_datasource.mdx index 071d1b9de..b0223a381 100644 --- a/website/versioned_docs/version-2.0.0/recipes/custom_datasource.mdx +++ b/website/versioned_docs/version-2.0.0/recipes/custom_datasource.mdx @@ -34,7 +34,7 @@ implementation when fetching and saving data for a specific path only: import CodeBlock from "@theme/CodeBlock"; import CustomDataSource - from "!!raw-loader!../../../../examples/example/src/docs/recipes/custom_datasource/custom_datasource"; + from "!!raw-loader!../../../samples/samples_v2/recipes/custom_datasource/custom_datasource"; {CustomDataSource} diff --git a/website/versioned_docs/version-2.0.0/recipes/documents_as_subcollections.mdx b/website/versioned_docs/version-2.0.0/recipes/documents_as_subcollections.mdx index a30bbb485..309d33bfb 100644 --- a/website/versioned_docs/version-2.0.0/recipes/documents_as_subcollections.mdx +++ b/website/versioned_docs/version-2.0.0/recipes/documents_as_subcollections.mdx @@ -40,11 +40,11 @@ How cool is that? import CodeBlock from "@theme/CodeBlock"; import UnitCollection - from "!!raw-loader!../../../../examples/example/src/docs/recipes/document_as_subcollection/unit_collection"; + from "!!raw-loader!../../../samples/samples_v2/recipes/document_as_subcollection/unit_collection"; import CollectionBuilder - from "!!raw-loader!../../../../examples/example/src/docs/recipes/document_as_subcollection/collection_builder"; + from "!!raw-loader!../../../samples/samples_v2/recipes/document_as_subcollection/collection_builder"; import FullCode - from "!!raw-loader!../../../../examples/example/src/docs/recipes/document_as_subcollection/full"; + from "!!raw-loader!../../../samples/samples_v2/recipes/document_as_subcollection/full"; {UnitCollection}