@firecms/entity_history 3.0.1 → 3.1.0-canary.02232f4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,8 @@ import {
7
7
  ErrorBoundary,
8
8
  useAuthController,
9
9
  useDataSource,
10
- useSnackbarController
10
+ useSnackbarController,
11
+ useTranslation
11
12
  } from "@firecms/core";
12
13
  import { cls, HistoryIcon, IconButton, Label, Tooltip, Typography } from "@firecms/ui";
13
14
  import { EntityHistoryEntry } from "./EntityHistoryEntry";
@@ -20,6 +21,7 @@ export function EntityHistoryView({
20
21
 
21
22
  const authController = useAuthController();
22
23
  const snackbarController = useSnackbarController();
24
+ const { t } = useTranslation();
23
25
  const dirty = formContext?.formex.dirty;
24
26
 
25
27
  const dataSource = useDataSource();
@@ -114,7 +116,7 @@ export function EntityHistoryView({
114
116
 
115
117
  if (!entity) {
116
118
  return <div className="flex items-center justify-center h-full">
117
- <Label>History is only available for existing entities</Label>
119
+ <Label>{t("entity_history_only_existing")}</Label>
118
120
  </div>
119
121
  }
120
122
 
@@ -152,14 +154,14 @@ export function EntityHistoryView({
152
154
  });
153
155
  setRevertVersionDialog(undefined);
154
156
  snackbarController.open({
155
- message: "Reverted version",
157
+ message: t("entity_history_reverted"),
156
158
  type: "info"
157
159
  });
158
160
  }
159
161
  ).catch((error) => {
160
162
  console.error("Error reverting entity:", error);
161
163
  snackbarController.open({
162
- message: "Error reverting entity",
164
+ message: t("entity_history_error_reverting"),
163
165
  type: "error"
164
166
  });
165
167
  });
@@ -172,15 +174,15 @@ export function EntityHistoryView({
172
174
  <div className="flex flex-col gap-2 max-w-6xl mx-auto w-full">
173
175
 
174
176
  <Typography variant={"h5"} className={"mt-24 ml-4"}>
175
- History
177
+ {t("history")}
176
178
  </Typography>
177
179
 
178
180
  {revisions.length === 0 && <>
179
181
  <Label className={"ml-4 mt-8"}>
180
- No history available
182
+ {t("entity_history_no_history")}
181
183
  </Label>
182
184
  <Typography variant={"caption"} className={"ml-4"}>
183
- When you save an entity, a new version is created and stored in the history.
185
+ {t("entity_history_when_save")}
184
186
  </Typography>
185
187
  </>}
186
188
 
@@ -194,13 +196,13 @@ export function EntityHistoryView({
194
196
  previewKeys={previewKeys}
195
197
  previousValues={previousValues}
196
198
  actions={
197
- <Tooltip title={"Revert to this version"}
199
+ <Tooltip title={t("entity_history_revert_tooltip")}
198
200
  className={"m-2 grow-0 self-start"}>
199
201
  <IconButton
200
202
  onClick={() => {
201
203
  if (dirty) {
202
204
  snackbarController.open({
203
- message: "Please save or discard your changes before reverting",
205
+ message: t("entity_history_please_save"),
204
206
  type: "warning"
205
207
  });
206
208
  } else {
@@ -220,8 +222,8 @@ export function EntityHistoryView({
220
222
  ref={loadMoreRef}
221
223
  className="py-4 text-center"
222
224
  >
223
- {isLoading && <Label>Loading more...</Label>}
224
- {!hasMore && revisions.length > PAGE_SIZE && <Label>No more history available</Label>}
225
+ {isLoading && <Label>{t("loading_more")}</Label>}
226
+ {!hasMore && revisions.length > PAGE_SIZE && <Label>{t("entity_history_no_more")}</Label>}
225
227
  </div>
226
228
  )}
227
229
  </div>
@@ -235,7 +237,7 @@ export function EntityHistoryView({
235
237
  onCancel={function (): void {
236
238
  setRevertVersionDialog(undefined);
237
239
  }}
238
- title={<Typography variant={"subtitle2"}>Revert data to this version?</Typography>}
240
+ title={<Typography variant={"subtitle2"}>{t("entity_history_revert_dialog_title")}</Typography>}
239
241
  body={revertVersionDialog ?
240
242
  <EntityView entity={revertVersionDialog}
241
243
  collection={collection}
@@ -0,0 +1,96 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Entity, useDataSource, User, useTranslation } from "@firecms/core";
3
+ import { useHistoryController } from "../HistoryControllerProvider";
4
+
5
+ function getRelativeTimeString(date: Date, t: any): string {
6
+ const now = new Date();
7
+ const diffMs = now.getTime() - date.getTime();
8
+ const diffSeconds = Math.floor(diffMs / 1000);
9
+ const diffMinutes = Math.floor(diffSeconds / 60);
10
+ const diffHours = Math.floor(diffMinutes / 60);
11
+ const diffDays = Math.floor(diffHours / 24);
12
+
13
+ if (diffSeconds < 60) return t("entity_history_just_now");
14
+ if (diffMinutes < 60) return t("entity_history_minutes_ago", { minutes: diffMinutes });
15
+ if (diffHours < 24) return t("entity_history_hours_ago", { hours: diffHours });
16
+ if (diffDays < 30) return t("entity_history_days_ago", { days: diffDays });
17
+ return date.toLocaleDateString();
18
+ }
19
+
20
+ /**
21
+ * Fetches the latest history entry from the __history subcollection
22
+ * and displays who last edited the entity and when.
23
+ */
24
+ export function LastEditedByIndicator({
25
+ path,
26
+ entityId,
27
+ collection
28
+ }: {
29
+ path: string;
30
+ entityId: string;
31
+ collection: any;
32
+ }) {
33
+ const { t } = useTranslation();
34
+ const { getUser } = useHistoryController();
35
+ const dataSource = useDataSource();
36
+ const [latestEntry, setLatestEntry] = useState<Entity | undefined>();
37
+
38
+ useEffect(() => {
39
+ if (!path || !entityId) return;
40
+
41
+ const historyPath = `${path}/${entityId}/__history`;
42
+ const unsubscribe = dataSource.listenCollection?.({
43
+ path: historyPath,
44
+ collection,
45
+ orderBy: "__metadata.updated_on",
46
+ order: "desc",
47
+ limit: 1,
48
+ onUpdate: (entities) => {
49
+ setLatestEntry(entities[0]);
50
+ },
51
+ onError: (error) => {
52
+ console.error("Error fetching latest history entry:", error);
53
+ }
54
+ });
55
+
56
+ return () => {
57
+ if (typeof unsubscribe === "function") {
58
+ unsubscribe();
59
+ }
60
+ };
61
+ }, [path, entityId, dataSource]);
62
+
63
+ const metadata = latestEntry?.values?.__metadata;
64
+ const uid = metadata?.updated_by;
65
+ const editedOn = metadata?.updated_on;
66
+
67
+ const hasData = Boolean(uid || editedOn);
68
+
69
+ const user: User | null | undefined = uid ? getUser?.(uid) : undefined;
70
+ const date = editedOn instanceof Date ? editedOn : (editedOn?.toDate ? editedOn.toDate() : null);
71
+ const timeString = date ? getRelativeTimeString(date, t) : null;
72
+
73
+ const displayName = user?.displayName ?? user?.email ?? uid;
74
+ const photoURL = user?.photoURL;
75
+
76
+ return (
77
+ <div className="flex items-center gap-2 text-xs text-text-secondary dark:text-text-secondary-dark min-h-6">
78
+ {hasData && <>
79
+ {photoURL ? (
80
+ <img
81
+ src={photoURL}
82
+ alt={displayName ?? "User"}
83
+ className="rounded-full object-cover w-6 h-6"
84
+ />
85
+ ) : (
86
+ <div className="rounded-full bg-primary/10 dark:bg-primary-dark/20 flex items-center justify-center text-primary dark:text-primary-dark font-medium w-6 h-6 text-xs">
87
+ {(displayName ?? "?").charAt(0).toUpperCase()}
88
+ </div>
89
+ )}
90
+ <span>
91
+ {displayName}{timeString ? ` · ${timeString}` : ""}
92
+ </span>
93
+ </>}
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { PluginFormActionProps } from "@firecms/core";
3
+ import { LastEditedByIndicator } from "./LastEditedByIndicator";
4
+
5
+ /**
6
+ * Renders the "last edited by" indicator in the entity form top bar.
7
+ * Used as a plugin `form.ActionsTop` component.
8
+ */
9
+ export function LastEditedByFormAction({
10
+ entityId,
11
+ path,
12
+ status,
13
+ collection,
14
+ }: PluginFormActionProps) {
15
+ if (status === "new" || status === "copy" || !entityId) return null;
16
+ if (!collection.history) return null;
17
+
18
+ return <LastEditedByIndicator
19
+ path={path}
20
+ entityId={entityId}
21
+ collection={collection}
22
+ />;
23
+ }
@@ -1,13 +1,14 @@
1
- import { User } from "@firecms/core";
1
+ import { User, useTranslation } from "@firecms/core";
2
2
  import { Chip, Tooltip } from "@firecms/ui";
3
3
 
4
4
  export function UserChip({ user }: { user: User }) {
5
+ const { t } = useTranslation();
5
6
  return (
6
7
  <Tooltip title={user.email ?? user.uid}>
7
8
  <Chip size={"small"} className={"flex items-center"}>
8
9
  {user.photoURL && <img
9
10
  className={"rounded-full w-6 h-6 mr-2"}
10
- src={user.photoURL} alt={user.displayName ?? "User picture"}/>}
11
+ src={user.photoURL} alt={user.displayName ?? t("user_picture")}/>}
11
12
  <span>{user.displayName ?? user.email ?? user.uid}</span>
12
13
  </Chip>
13
14
  </Tooltip>
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./useEntityHistoryPlugin";
2
2
  export * from "./HistoryControllerProvider";
3
3
  export { createHistoryEntry } from "./entity_history_callbacks";
4
+ export { LastEditedByIndicator } from "./components/LastEditedByIndicator";
4
5
  export * from "./types";
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsDe = {
2
+ history: "Verlauf",
3
+ entity_history_only_existing: "Verlauf ist nur für vorhandene Entitäten verfügbar",
4
+ entity_history_reverted: "Wiederhergestellte Version",
5
+ entity_history_error_reverting: "Fehler beim Wiederherstellen der Entität",
6
+ entity_history_no_history: "Kein Verlauf verfügbar",
7
+ entity_history_when_save: "Wenn Sie eine Entität speichern, wird eine neue Version erstellt und im Verlauf gespeichert.",
8
+ entity_history_revert_tooltip: "Zu dieser Version zurückkehren",
9
+ entity_history_please_save: "Bitte speichern oder verwerfen Sie Ihre Änderungen vor der Wiederherstellung",
10
+ loading_more: "Mehr laden...",
11
+ entity_history_no_more: "Kein weiterer Verlauf verfügbar",
12
+ entity_history_revert_dialog_title: "Daten auf diese Version zurücksetzen?",
13
+ entity_history_previous_value: "Vorheriger Wert",
14
+ entity_history_see_details: "Details zu dieser Überarbeitung anzeigen",
15
+ entity_history_just_now: "gerade eben",
16
+ entity_history_minutes_ago: "vor {{minutes}}m",
17
+ entity_history_hours_ago: "vor {{hours}}h",
18
+ entity_history_days_ago: "vor {{days}}d",
19
+ user_picture: "Benutzerbild"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsEn = {
2
+ history: "History",
3
+ entity_history_only_existing: "History is only available for existing entities",
4
+ entity_history_reverted: "Reverted version",
5
+ entity_history_error_reverting: "Error reverting entity",
6
+ entity_history_no_history: "No history available",
7
+ entity_history_when_save: "When you save an entity, a new version is created and stored in the history.",
8
+ entity_history_revert_tooltip: "Revert to this version",
9
+ entity_history_please_save: "Please save or discard your changes before reverting",
10
+ loading_more: "Loading more...",
11
+ entity_history_no_more: "No more history available",
12
+ entity_history_revert_dialog_title: "Revert data to this version?",
13
+ entity_history_previous_value: "Previous value",
14
+ entity_history_see_details: "See details for this revision",
15
+ entity_history_just_now: "just now",
16
+ entity_history_minutes_ago: "{{minutes}}m ago",
17
+ entity_history_hours_ago: "{{hours}}h ago",
18
+ entity_history_days_ago: "{{days}}d ago",
19
+ user_picture: "User picture"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsEs = {
2
+ history: "Historial",
3
+ entity_history_only_existing: "El historial solo está disponible para entidades existentes",
4
+ entity_history_reverted: "Versión revertida",
5
+ entity_history_error_reverting: "Error al revertir la entidad",
6
+ entity_history_no_history: "No hay historial disponible",
7
+ entity_history_when_save: "Cuando guardas una entidad, se crea una nueva versión y se almacena en el historial.",
8
+ entity_history_revert_tooltip: "Revertir a esta versión",
9
+ entity_history_please_save: "Por favor, guarda o descarta tus cambios antes de revertir",
10
+ loading_more: "Cargando más...",
11
+ entity_history_no_more: "No hay más historial disponible",
12
+ entity_history_revert_dialog_title: "¿Revertir datos a esta versión?",
13
+ entity_history_previous_value: "Valor anterior",
14
+ entity_history_see_details: "Ver detalles de esta revisión",
15
+ entity_history_just_now: "justo ahora",
16
+ entity_history_minutes_ago: "hace {{minutes}}m",
17
+ entity_history_hours_ago: "hace {{hours}}h",
18
+ entity_history_days_ago: "hace {{days}}d",
19
+ user_picture: "Foto de usuario"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsFr = {
2
+ history: "Historique",
3
+ entity_history_only_existing: "L'historique n'est disponible que pour les entités existantes",
4
+ entity_history_reverted: "Version restaurée",
5
+ entity_history_error_reverting: "Erreur lors de la restauration de l'entité",
6
+ entity_history_no_history: "Aucun historique disponible",
7
+ entity_history_when_save: "Lorsque vous enregistrez une entité, une nouvelle version est créée et stockée dans l'historique.",
8
+ entity_history_revert_tooltip: "Revenir à cette version",
9
+ entity_history_please_save: "Veuillez enregistrer ou annuler vos modifications avant de restaurer",
10
+ loading_more: "Chargement en cours...",
11
+ entity_history_no_more: "Aucun autre historique disponible",
12
+ entity_history_revert_dialog_title: "Restaurer les données à cette version ?",
13
+ entity_history_previous_value: "Valeur précédente",
14
+ entity_history_see_details: "Voir les détails de cette révision",
15
+ entity_history_just_now: "à l'instant",
16
+ entity_history_minutes_ago: "il y a {{minutes}}m",
17
+ entity_history_hours_ago: "il y a {{hours}}h",
18
+ entity_history_days_ago: "il y a {{days}}j",
19
+ user_picture: "Photo de l'utilisateur"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsHi = {
2
+ history: "इतिहास",
3
+ entity_history_only_existing: "इतिहास केवल मौजूद संस्थाओं के लिए उपलब्ध है",
4
+ entity_history_reverted: "परिवर्तित संस्करण",
5
+ entity_history_error_reverting: "संस्था को वापस लाने में त्रुटि",
6
+ entity_history_no_history: "कोई इतिहास उपलब्ध नहीं",
7
+ entity_history_when_save: "जब आप किसी संस्था को सहेजते हैं, तो एक नया संस्करण बनाया जाता है और इतिहास में संग्रहीत किया जाता है।",
8
+ entity_history_revert_tooltip: "इस संस्करण पर वापस जाएं",
9
+ entity_history_please_save: "कृपया वापस जाने से पहले अपने परिवर्तनों को सहेजें या हटा दें",
10
+ loading_more: "और लोड हो रहा है...",
11
+ entity_history_no_more: "कोई और इतिहास उपलब्ध नहीं",
12
+ entity_history_revert_dialog_title: "डेटा को इस संस्करण में वापस लाएँ?",
13
+ entity_history_previous_value: "पिछला मान",
14
+ entity_history_see_details: "इस संशोधन के लिए विवरण यहाँ देखें",
15
+ entity_history_just_now: "अभी-अभी",
16
+ entity_history_minutes_ago: "{{minutes}}m पहले",
17
+ entity_history_hours_ago: "{{hours}}h पहले",
18
+ entity_history_days_ago: "{{days}}d पहले",
19
+ user_picture: "उपयोगकर्ता चित्र"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsIt = {
2
+ history: "Cronologia",
3
+ entity_history_only_existing: "La cronologia è disponibile solo per le entità esistenti",
4
+ entity_history_reverted: "Versione ripristinata",
5
+ entity_history_error_reverting: "Errore durante il ripristino dell'entità",
6
+ entity_history_no_history: "Nessuna cronologia disponibile",
7
+ entity_history_when_save: "Quando salvi un'entità, viene creata una nuova versione e memorizzata nella cronologia.",
8
+ entity_history_revert_tooltip: "Ripristina a questa versione",
9
+ entity_history_please_save: "Salva o annulla le modifiche prima del ripristino",
10
+ loading_more: "Caricamento in corso...",
11
+ entity_history_no_more: "Nessuna ulteriore cronologia disponibile",
12
+ entity_history_revert_dialog_title: "Vuoi ripristinare i dati a questa versione?",
13
+ entity_history_previous_value: "Valore precedente",
14
+ entity_history_see_details: "Vedi i dettagli per questa revisione",
15
+ entity_history_just_now: "proprio adesso",
16
+ entity_history_minutes_ago: "{{minutes}} min fa",
17
+ entity_history_hours_ago: "{{hours}} h fa",
18
+ entity_history_days_ago: "{{days}} g fa",
19
+ user_picture: "Immagine utente"
20
+ };
@@ -0,0 +1,20 @@
1
+ export const entityHistoryTranslationsPt = {
2
+ history: "Histórico",
3
+ entity_history_only_existing: "O histórico está disponível apenas para entidades existentes",
4
+ entity_history_reverted: "Versão revertida",
5
+ entity_history_error_reverting: "Erro ao reverter entidade",
6
+ entity_history_no_history: "Sem histórico disponível",
7
+ entity_history_when_save: "Quando guarda uma entidade, é criada uma nova versão e armazenada no histórico.",
8
+ entity_history_revert_tooltip: "Reverter para esta versão",
9
+ entity_history_please_save: "Por favor guarde ou descarte as suas alterações antes de reverter",
10
+ loading_more: "A carregar mais...",
11
+ entity_history_no_more: "Sem mais histórico disponível",
12
+ entity_history_revert_dialog_title: "Reverter dados para esta versão?",
13
+ entity_history_previous_value: "Valor anterior",
14
+ entity_history_see_details: "Ver detalhes desta revisão",
15
+ entity_history_just_now: "agora mesmo",
16
+ entity_history_minutes_ago: "há {{minutes}}m",
17
+ entity_history_hours_ago: "há {{hours}}h",
18
+ entity_history_days_ago: "há {{days}}d",
19
+ user_picture: "Foto do utilizador"
20
+ };
package/src/types.ts CHANGED
@@ -19,4 +19,3 @@ export interface NewHistoryEntryParams<T = any> {
19
19
  entityId: string;
20
20
  collection?: EntityCollection;
21
21
  }
22
-
@@ -1,27 +1,35 @@
1
1
  import { useCallback, useMemo } from "react";
2
- import { EntityCollection, FireCMSPlugin, mergeCallbacks, User } from "@firecms/core";
2
+ import { EntityCollection, FireCMSPlugin, mergeCallbacks, User, useTranslation } from "@firecms/core";
3
3
  import { EntityHistoryView } from "./components/EntityHistoryView";
4
4
  import { HistoryIcon } from "@firecms/ui";
5
5
  import { entityHistoryCallbacks } from "./entity_history_callbacks";
6
6
  import { HistoryControllerProvider } from "./HistoryControllerProvider";
7
+ import { LastEditedByFormAction } from "./components/LastEditedByPluginComponents";
8
+ import { entityHistoryTranslationsEn } from "./locales/en";
9
+ import { entityHistoryTranslationsEs } from "./locales/es";
10
+ import { entityHistoryTranslationsDe } from "./locales/de";
11
+ import { entityHistoryTranslationsFr } from "./locales/fr";
12
+ import { entityHistoryTranslationsIt } from "./locales/it";
13
+ import { entityHistoryTranslationsHi } from "./locales/hi";
14
+ import { entityHistoryTranslationsPt } from "./locales/pt";
7
15
 
8
16
  /**
9
17
  * This plugin adds a history view to the entity side panel.
10
18
  */
11
19
  export function useEntityHistoryPlugin(props?: EntityHistoryPluginProps): FireCMSPlugin<any, any, any, EntityHistoryPluginProps> {
12
-
13
20
  const { defaultEnabled = false } = props ?? {};
14
21
 
15
22
  const modifyCollection = useCallback((collection: EntityCollection) => {
16
23
  if (collection.history === true || (defaultEnabled && collection.history !== false)) {
17
24
  return {
18
25
  ...collection,
26
+ history: true,
19
27
  entityViews: [
20
28
  ...(collection.entityViews ?? []),
21
29
  {
22
30
  key: "__history",
23
31
  name: "History",
24
- tabComponent: <HistoryIcon size={"small"}/>,
32
+ tabComponent: <HistoryIcon size={"small"} />,
25
33
  Builder: EntityHistoryView,
26
34
  position: "start"
27
35
  }
@@ -40,8 +48,20 @@ export function useEntityHistoryPlugin(props?: EntityHistoryPluginProps): FireCM
40
48
  getUser: props?.getUser
41
49
  }
42
50
  },
51
+ form: {
52
+ BeforeTitle: LastEditedByFormAction
53
+ },
43
54
  collection: {
44
55
  modifyCollection
56
+ },
57
+ i18n: {
58
+ en: entityHistoryTranslationsEn,
59
+ es: entityHistoryTranslationsEs,
60
+ de: entityHistoryTranslationsDe,
61
+ fr: entityHistoryTranslationsFr,
62
+ it: entityHistoryTranslationsIt,
63
+ hi: entityHistoryTranslationsHi,
64
+ pt: entityHistoryTranslationsPt
45
65
  }
46
66
  } satisfies FireCMSPlugin), [props]);
47
67
  }