FireCMS
Awesome Firestore based headless CMS, developed by Camberi
FireCMS is a headless CMS and admin panel built by developers for developers. It generates CRUD views based on your configuration. You define views that are mapped to absolute or relative paths in your Firestore database, as well as schemas for your entities.
Note that this is a full application, with routing enabled and not a simple component. It has only been tested as an application and not as part of a different one.
It is currently in an alpha state, and we continue working to add features and expose internal APIs, so it is safe to expect breaking changes.
Core technologies
FireCMS is based on this great technologies:
- Typescript
- Firebase
- React + React Router
- Material UI
- Formik + Yup
Demo
Check the demo with all the core functionalities. You can modify the data, but it gets periodically restored.
https://firecms-demo-27150.web.app/
Changelog
https://github.com/Camberi/firecms/blob/master/CHANGELOG.md
Install
In your React project, simply install the dependency.
npm install @camberi/firecms
or
yarn add @camberi/firecms
Use
FireCMS is a purely a React app that uses your Firebase project as a backend, so you do not need a specific backend to make it run. Just build your project following the installation instructions and deploy it in the way you prefer. A very easy way is using Firebase Hosting.
Firebase requirements
You need to enable the Firestore database in your Firebase project. If you have enabled authentication in the CMS config you need to enable Google authentication in your project.
Also, if you are using storage fields in your string properties, you need to enable Firebase Storage.
Deployment to Firebase hosting
If you are deploying this project to firebase hosting, and the app it properly linked to ir, you can omit the firebaseConfig specification, since it gets picked up automatically.
Features
- Create, read, update, delete views
- Form for editing entities
- Implementation of fields for every property (except Geopoint)
- Native support for Google Storage references and file upload.
- Real-time Collection view for entities
- Inline editing
- Hooks on pre and post saving and deletion of entities
- Collection text search integration
- Infinite scrolling in collections
- Drag and drop reordering of arrays
- Custom additional views in main navigation
- Custom fields defined by the developer.
- Subcollection support
- Filters (string, numbers and booleans)
- Filters for arrays, dates
- All login methods supported by Firebase
- Custom authenticator to control access
- Validation for required fields using yup
- Allow set up of a project using a CLI create-firecms-app
Quick example
import React from "react";
import {
Authenticator,
buildCollection,
buildSchema,
CMSApp,
EntityCollectionView
} from "@camberi/firecms";
import firebase from "firebase/app";
import "typeface-rubik";
// Replace with your config
const firebaseConfig = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "",
messagingSenderId: "",
appId: "",
measurementId: ""
};
const locales = {
"de-DE": "German",
"en-US": "English (United States)",
"es-ES": "Spanish (Spain)",
"es-419": "Spanish (South America)"
};
const productSchema = buildSchema({
name: "Product",
properties: {
name: {
title: "Name",
validation: { required: true },
dataType: "string"
},
price: {
title: "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: {
title: "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.",
config: {
enumValues: {
private: "Private",
public: "Public"
}
}
},
categories: {
title: "Categories",
validation: { required: true },
dataType: "array",
of: {
dataType: "string",
config: {
enumValues: {
electronics: "Electronics",
books: "Books",
furniture: "Furniture",
clothing: "Clothing",
food: "Food"
}
}
}
},
image: {
title: "Image",
dataType: "string",
config: {
storageMeta: {
mediaType: "image",
storagePath: "images",
acceptedFiles: ["image/*"]
}
}
},
tags: {
title: "Tags",
description: "Example of generic array",
validation: { required: true },
dataType: "array",
of: {
dataType: "string"
}
},
description: {
title: "Description",
description: "Not mandatory but it'd be awesome if you filled this up",
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
},
published: {
title: "Published",
dataType: "boolean",
columnWidth: 100
},
publisher: {
title: "Publisher",
description: "This is an example of a map property",
dataType: "map",
properties: {
name: {
title: "Name",
dataType: "string"
},
external_id: {
title: "External id",
dataType: "string"
}
}
},
expires_on: {
title: "Expires on",
dataType: "timestamp"
}
}
});
const localeSchema = buildSchema({
customId: locales,
name: "Locale",
properties: {
title: {
title: "Title",
validation: { required: true },
dataType: "string"
},
selectable: {
title: "Selectable",
description: "Is this locale selectable",
dataType: "boolean"
},
video: {
title: "Video",
dataType: "string",
validation: { required: false },
config: {
storageMeta: {
mediaType: "video",
storagePath: "videos",
acceptedFiles: ["video/*"]
}
}
}
}
});
export function SimpleApp() {
const navigation: EntityCollectionView[] = [
buildCollection({
relativePath: "products",
schema: productSchema,
name: "Products",
subcollections: [
buildCollection({
name: "Locales",
relativePath: "locales",
schema: localeSchema
})
]
})
];
const myAuthenticator: Authenticator = (user?: firebase.User) => {
console.log("Allowing access to", user?.email);
return true;
};
return <CMSApp
name={"My Online Shop"}
authentication={myAuthenticator}
navigation={navigation}
firebaseConfig={firebaseConfig}
/>;
}
ReactDOM.render(
<SimpleApp/>,
document.getElementById("root")
);
Included example
You can access the code for the demo project under
example. It includes
every feature provided by this CMS.
To get going you just need to set you Firebase config in firebase_config.ts
and run yarn start.
Real time support
Every view in the CMS has real time data support. This makes it suitable for displaying data that needs to be always updated.
Forms also support this feature, any modified value in the database will be updated in any currently open form view, as long as it has not been touched by the user. This makes it suitable for advanced cases where you trigger a Cloud Function after saving an entity that modifies some values, and you want to get real time updates.
CMSApp level configuration
The entry point for setting up a FireCMS app is the CMSApp, where you can define the following specs:
-
nameName of the app, displayed as the main title and in the tab title. -
navigationList of the views in the CMS. Each view relates to a collection in the root Firestore database. Each of the navigation entries in this field generates an entry in the main menu. -
logoLogo to be displayed in the drawer of the CMS. -
authenticationDo the users need to log in to access the CMS. You can specify an Authenticator function to discriminate which users can access the CMS or not. If not specified, authentication is enabled but no user restrictions apply. -
signInOptionsList of sign in options that will be displayed in the login view ifauthenticationis enabled. You can pass google providers strings, such asfirebase.auth.GoogleAuthProvider.PROVIDER_IDor full configuration objects such as specified in https://firebase.google.com/docs/auth/web/firebaseui Defaults to Google sign in only. -
allowSkipLoginIf authentication is enabled, allow the user to access the content without login. -
additionalViewsCustom additional views created by the developer, added to the main navigation. -
firebaseConfigFirebase configuration of the project. If you afe deploying the app to Firebase hosting, you don't need to specify this value. -
onFirebaseInitAn optional callback after Firebase has been initialised. Useful for using the local emulator or retrieving the used configuration. -
primaryColorPrimary color of the theme of the CMS. -
secondaryColorPrimary color of the theme of the CMS. -
fontFamilyFont family string. e.g. '"Roboto", "Helvetica", "Arial", sans-serif'. -
toolbarExtraWidgetA component that gets rendered on the upper side of the main toolbar.
Entities configuration
The core of the CMS are entities, which are defined by an EntitySchema. In the
schema you define the properties, which are related to the Firestore data types.
-
nameA singular name of the entity as displayed in an Add button. E.g. Product -
descriptionDescription of this entity. -
customIdWhen not specified, Firestore will create a random ID. You can set the value totrueto allow the users to choose the ID. You can also pass a set of values (as anEnumValuesobject) to allow them to pick from only those. -
propertiesObject defining the properties for the entity schema.
Entity properties
You can specify the properties of an entity, using the following configuration fields, common to all data types:
-
dataTypeFirestore datatype of the property. -
titleProperty title (e.g. Product). -
descriptionProperty description. -
longDescriptionWidth in pixels of this column in the collection view. If not set the width is inferred based on the other configurations. -
columnWidthLonger description of a field, displayed under a popover. -
disabledIs this a read only property. -
config-
fieldIf you need to render a custom field, you can pass a React component taking CMSFieldProps as props. More details below. -
fieldPropsAdditional props that are passed to the default field generated by FireCMS or to the custom field. -
customPreviewConfigure how a property is displayed as a preview, e.g. in the collection view.
-
-
onPreSaveHook called before saving, you need to return the values that will get saved. If you throw an error in this method the process stops, and an error snackbar gets displayed. (example bellow) -
onSaveSuccessHook called when save is successful. -
onPreSaveHook called when saving fails. -
defaultValuesObject defining the initial values of the entity on creation.
Property configurations
Beside the common fields, some properties have specific configurations.
string
-
configstorageMetaYou can specify aStorageMetaconfiguration. It is used to indicate that this string refers to a path in Google Cloud Storage.mediaTypeMedia type of this reference, used for displaying the preview.storagePathAbsolute path in your bucket.acceptedFilesFile MIME types that can be uploaded to this reference.metadataSpecific metadata set in your uploaded file.storeUrlWhen set totrue, this flag indicates that the download URL of the file will be saved in Firestore instead of the Cloud storage path. Note that the generated URL may use a token that, if disabled, may make the URL unusable and lose the original reference to Cloud Storage, so it is not encouraged to use this flag. Defaults to false.
urlIf the value of this property is a URL, you can set this flag totrueto add a link, or one of the supported media types to render a preview.enumValuesYou can use the enum values providing a map of possible exclusive values the property can take, mapped to the label that it is displayed in the dropdown.multilineIs this string property long enough, so it should be displayed in a multiple line field. Defaults to false. If set totrue, the number of lines adapts to the content.markdownShould this string property be displayed as a markdown field. Iftrue, the field is rendered as a text editors that supports markdown highlight syntax. It also includes a preview of the result.previewAsTagShould this string be rendered as a tag instead of just text.
-
validationRules for validating this property:requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.lengthSet a required length for the string value.minSet a minimum length limit for the string value.maxSet a maximum length limit for the string value.matchesProvide an arbitrary regex to match the value against.emailValidates the value as an email address via a regex.urlValidates the value as a valid URL via a regex.trimTransforms string values by removing leading and trailing whitespace.lowercaseTransforms the string value to lowercase.uppercaseTransforms the string value to uppercase.
number
-
configenumValuesYou can use the enum values providing a map of possible exclusive values the property can take, mapped to the label that it is displayed in the dropdown.
-
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.minSet the minimum value allowed.maxSet the maximum value allowed.lessThanValue must be less than.moreThanValue must be more than.positiveValue must be a positive number.negativeValue must be a negative number.integerValue must be an integer.
boolean
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.
timestamp
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.minSet the minimum date allowed.maxSet the maximum date allowed.
reference
-
collectionPathAbsolute collection path of the collection this reference points to. The schema of the entity is inferred based on the root navigation, so the filters and search delegate existing there are applied to this view as well. -
previewPropertiesList of properties rendered as this reference preview. Defaults to first 3. -
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.
array
-
ofThe property of this array. You can specify any property. You can also specify an array or properties if you need the array to have a specific limited shape such as [string, number, string]. -
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.minSet the minimum length allowed.maxSet the maximum length allowed.
map
-
propertiesRecord of properties included in this map. -
previewPropertiesList of properties rendered as this map preview. Defaults to first 3. -
validationRules for validating this property.requiredShould this field be compulsory.requiredMessageMessage to be displayed as a validation error.
geopoint
THIS PROPERTY IS CURRENTLY NOT SUPPORTED
Custom fields
If you need a custom field for your property you can do it by passing a React
component to the field prop of a property config. The React component must
accept the props of type CMSFieldProps, which you can extend with your own
props. The bare minimum you need to implement is a field that displays the
received value and uses the setValue callback.
See how it works in this sample custom text field
You can find the all
the CMSFieldProps here
You can also pass custom props to your custom field, which you then receive in
the customProps.
If you are developing a custom field and need to access the values of the entity,
you can use the context field in CMSFieldProps.
Saving callbacks
When you are saving an entity you can attach diff
erent hooks before and after it
gets saved: onPreSave, onSaveSuccess and onSaveFailure.
const productSchema = buildSchema({
customId: true,
name: "Product",
properties: {
name: {
title: "Name",
validation: { required: true },
dataType: "string"
},
uppercase_name: {
title: "Uppercase Name",
dataType: "string",
disabled: true,
description: "This field gets updated with a preSave hook"
},
}
});
productSchema.onPreSave = ({
schema,
collectionPath,
id,
values,
status
}: EntitySaveProps<typeof productSchema>) => {
values.uppercase_name = values.name.toUpperCase();
return values;
};
Collection configuration
Once you have defined at least one entity schema, you can include it in a collection. You can find collection views as the first level of navigation in the main menu, or as subcollections inside other collections, following the Firestore data schema.
-
nameThe plural name of the view. E.g. 'products'. -
relativePathRelative Firestore path of this view to its parent. If this view is in the root the path is equal to the absolute one. This path also determines the URL in FireCMS. -
defaultSizeDefault size of the rendered collection. -
sizeOptional field used to group top level navigation entries under a navigation view. If you set this value in a subcollection it has no effect. -
groupOptional field used to group top level navigation entries under a navigation view. If you set this value in a subcollection it has no effect. -
propertiesProperties displayed in this collection. If this property is not set every property is displayed. -
excludedPropertiesProperties that should NOT get displayed in the collection view. All the other properties from the entity are displayed. It has no effect if thepropertiesvalue is set. -
filterablePropertiesList of properties that include a filter widget. Defaults to none. -
initialFilterInitial filters applied to this collection. Consider that you can filter any property, but only those included infilterablePropertieswill include the corresponding filter widget. Defaults to none -
initialSortDefault sort applied to this collection. It takes tuples in the shape["property_name", "asc"]or["property_name", "desc"] -
extraActionsBuilder for rendering additional components such as buttons in the collection toolbar. The builder takes an object with propsentityCollectionViewandselectedEntitiesif any are set by the end user. -
paginationIf enabled, content is loaded in batch. Iffalseall entities in the collection are loaded. Defaults totrue. -
additionalColumnsYou can add additional columns to the collection view by implementing an additional column delegate. -
textSearchDelegateIf a text search delegate is supplied, a search bar is displayed on top. -
editEnabledCan the elements in this collection be added and edited. Defaults totrue. -
inlineEditingCan the elements in this collection be edited inline in the collection view. If this flag is set to false buteditEnabledistrue, entities can still be edited in the side panel. -
deleteEnabledCan the elements in this collection be deleted. Defaults totrue. -
subcollectionsFollowing the Firestore document and collection schema, you can add subcollections to your entity in the same way you define the root collections. -
onEntityDeleteHook called after the entity gets deleted in Firestore.
Additional columns
If you would like to include a column that does not map directly to a property,
you can use the additionalColumns field, providing a
AdditionalColumnDelegate, which includes an id, a title, and a builder that
receives the corresponding entity.
In the builder you can return any React Component.
If you would like to do some async computation, such as fetching a different
entity, you can use the utility component AsyncPreviewComponent to show a
loading indicator.
Subcollections
Subcollections are collections of entities that are found under another entity.
For example, you can have a collection named "translations" under the entity
"Article". You just need to use the same format as for defining your collection
using the field subcollections.
Subcollections are easily accessible from the side view while editing an entity.
Filters
Filtering support is currently limited to string, number and boolean values, including enum types. If you want a property to be filterable, you can mark it as such in the entity schema.
Any comments related to this feature are welcome.
Text search
Firestore does not support native text search, so we need to rely on external
solutions. If you specify a textSearchDelegate to the collection view, you
will see a search bar on top. The delegate is in charge of returning the
matching ids, from the search string.
A delegate using AlgoliaSearch is included, where you need to specify your credentials and index. For this to work you need to set up an AlgoliaSearch account and manage the indexing of your documents. There is a full backend example included in the code, which indexes documents with Cloud Functions.
You can also implement your own TextSearchDelegate, and would love to hear how
you come around this problem.
Provided hooks
FireCMS provides different hooks that allow you to interact with the internal state of the app. Please note that in order to use this hook you must be in a component (you can't use them directly from a callback function).
Auth Context
useAuthContext
For state and operations regarding authentication.
The props provided by this context are:
loggedUserThe Firebase user currently logged in or nullauthProviderErrorError dispatched by the auth providerauthLoadingIs the login process ongoingloginSkippedIs the login skippednotAllowedErrorThe current user was not allowed accessskipLogin()Skip loginsignOut()Sign out
Example:
import React from "react";
import { useAuthContext } from "@camberi/firecms";
export function ExampleAdditionalView() {
const authContext = useAuthContext();
return (
authContext.loggedUser ?
<div>Logged in as {authContext.loggedUser.displayName}</div>
:
<div>You are not logged in</div>
);
}
Snackbar controller
useSnackbarController
For displaying snackbars
The props provided by this context are:
-
isOpenIs there currently an open snackbar -
close()Close the currently open snackbar -
open ({ type: "success" | "info" | "warning" | "error"; title?: string; message: string; })Display a new snackbar. You need to specify the type and message. You can optionally specify a title
Example:
import React from "react";
import { useSnackbarController } from "@camberi/firecms";
export function ExampleAdditionalView() {
const snackbarController = useSnackbarController();
return (
<Button
onClick={() => snackbarController.open({
type: "success",
title: "Hey!",
message: "Test snackbar"
})}>
Click me
</Button>
);
}
Contact
francesco@camberi.com
License
GPL-3.0 © camberi