@rebasepro/app 0.14.0 → 0.14.1

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.
@@ -105,11 +105,18 @@ export interface LoginViewProps {
105
105
  */
106
106
  defaultPassword?: string;
107
107
  /**
108
- * When set, the email/password forms render a "Join the newsletter"
109
- * opt-in checkbox. Called with the address that just authenticated —
110
- * sign-in or registration if the box was ticked. Wire it to whatever
111
- * stores the subscription; failures are the callback's problem, the
112
- * login flow never waits on it.
108
+ * When set, a "Join the newsletter" opt-in checkbox is offered. Called with
109
+ * the address that just authenticated — by any method — if the box was
110
+ * ticked. Wire it to whatever stores the subscription; failures are the
111
+ * callback's problem, the login flow never waits on it.
112
+ *
113
+ * The checkbox sits on the **provider screen**, beside the buttons that
114
+ * choose how to sign in, rather than on the credentials form. It is a
115
+ * decision about the visitor, not about the password: asking on the
116
+ * credentials form put it in front of only the people who got that far by
117
+ * typing an address, so anyone signing in with Google was never offered it
118
+ * at all. Bootstrap mode is the exception — there is no provider screen
119
+ * there, so the form keeps it.
113
120
  */
114
121
  onNewsletterOptIn?: (email: string) => void;
115
122
  }
@@ -1,4 +1,4 @@
1
- import type { FindParams, FilterValues, LogicalCondition } from "@rebasepro/types";
1
+ import type { FindParams, FilterValues, LogicalCondition, OrderBySpec } from "@rebasepro/types";
2
2
  /**
3
3
  * The one place a collection read's parameters are assembled.
4
4
  *
@@ -23,12 +23,12 @@ export interface CollectionQueryInput<M extends Record<string, unknown>> {
23
23
  logical?: LogicalCondition;
24
24
  /**
25
25
  * Loosened from `FindParams["orderBy"]` on purpose: callers hold the sort
26
- * as a plain `[string, direction]` read off user config or a URL, where the
27
- * column is not statically known. Narrowing it here would push a cast onto
28
- * every call site, which is the kind of friction that sends people back to
29
- * hand-building the object this exists to replace.
26
+ * as plain `[string, direction]` pairs read off user config or a URL, where
27
+ * the columns are not statically known. Narrowing it here would push a cast
28
+ * onto every call site, which is the kind of friction that sends people back
29
+ * to hand-building the object this exists to replace.
30
30
  */
31
- orderBy?: [string, "asc" | "desc"];
31
+ orderBy?: OrderBySpec;
32
32
  limit?: number;
33
33
  offset?: number;
34
34
  page?: number;
@@ -12,6 +12,6 @@
12
12
  * @internal
13
13
  */
14
14
  export declare function useTranslation(): {
15
- t: (key: string, vars?: Record<string, string>) => string;
15
+ t: (key: string, vars?: Record<string, string | number>) => string;
16
16
  i18n: import("i18next").i18n;
17
17
  };
package/dist/index.es.js CHANGED
@@ -4,7 +4,7 @@ import { ALL_WHERE_FILTER_OPS, DEFAULT_DATA_SOURCE_KEY, DEFAULT_FILTERABLE_RELAT
4
4
  import { UNRENDERED_SLOTS, resolveAdminCollection } from "@rebasepro/admin-types";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { SnackbarProvider as SnackbarProvider$1, useSnackbar } from "notistack";
7
- import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
7
+ import { buildRebaseData, canCreateEntity, canDeleteEntity, canEditEntity, canReadCollection, evaluateCondition, getChildViewDeclaringProperties, getChildViewRelationPropertyKeys, getEntityChildViews, getLabelOrConfigFrom, getPrimaryKeys, getSubcollections, isPropertyBuilder, normalizeOrderBy, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource, serializeOrderBy, stripCollectionPath, wrapAsEntityData, wrapAsSdkData } from "@rebasepro/common";
8
8
  import { getTitlePropertyKey as getTitlePropertyKey$1, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1 } from "@rebasepro/app";
9
9
  import { Link, Routes, createBrowserRouter, useBlocker, useLocation } from "react-router";
10
10
  import { generateForeignKeyName, hashString, isArrayValue, isObject, isPlainObject, isRecordValue, mergeDeep, randomString, readStoredJson, slugify, writeStoredJson } from "@rebasepro/utils";
@@ -2227,6 +2227,11 @@ function useTranslation() {
2227
2227
  * Also supports i18next interpolation variables, e.g.
2228
2228
  * t("add_to_field", { fieldName: "Tags" })
2229
2229
  * t("error_deleting", { message: err.message })
2230
+ * t("sort_key_position", { position: 2 })
2231
+ *
2232
+ * Numbers are accepted as well as strings: i18next interpolates them
2233
+ * either way, and a signature that took only strings pushed a `String()`
2234
+ * onto every call site with a count in it — `{{count}}`, `{{position}}`.
2230
2235
  */
2231
2236
  const typedT = useCallback((key, vars) => t(key, vars), [t]);
2232
2237
  return useMemo(() => ({
@@ -2859,11 +2864,12 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
2859
2864
  return true;
2860
2865
  }, []);
2861
2866
  const sortInternal = useMemo(() => {
2862
- if (sort && fixedFilter && !checkFilterCombination(fixedFilter, sort)) {
2867
+ const keys = normalizeOrderBy(sort);
2868
+ if (keys && fixedFilter && !checkFilterCombination(fixedFilter, keys)) {
2863
2869
  console.warn("Initial sort is not compatible with the force filter. Ignoring initial sort");
2864
2870
  return;
2865
2871
  }
2866
- return sort;
2872
+ return keys;
2867
2873
  }, [sort, fixedFilter]);
2868
2874
  const { filterValues: filterUrl, sortBy: sortUrl } = parseFilterAndSort(location.search);
2869
2875
  const [filterValues, setFilterValues] = React.useState(fixedFilter ?? (updateUrl ? filterUrl : void 0) ?? defaultFilter ?? void 0);
@@ -2900,8 +2906,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
2900
2906
  });
2901
2907
  }, []);
2902
2908
  const [itemCount, setItemCount] = React.useState(paginationEnabled ? initialItemCount : void 0);
2903
- const sortByProperty = sortBy ? sortBy[0] : void 0;
2904
- const currentSort = sortBy ? sortBy[1] : void 0;
2909
+ const sortKey = sortBy ? serializeOrderBy(sortBy) : void 0;
2905
2910
  const context = useRebaseContext();
2906
2911
  const [rawData, setRawData] = useState(collectionScroll?.data ?? []);
2907
2912
  const onScroll = useCallback(({ scrollOffset }) => {
@@ -2965,7 +2970,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
2965
2970
  };
2966
2971
  const accessor = dataClient.collection(path);
2967
2972
  const whereParams = filterValues && Object.keys(filterValues).length > 0 ? filterValues : void 0;
2968
- const orderByParams = sortBy ? [String(sortBy[0]), sortBy[1]] : void 0;
2973
+ const orderByParams = sortBy && sortBy.length > 0 ? sortBy.map(([field, direction]) => [String(field), direction]) : void 0;
2969
2974
  let unsubscribe;
2970
2975
  const includeParams = getRelationIncludeParams(collection);
2971
2976
  if (accessor.listen) unsubscribe = accessor.listen(toFindParams({
@@ -2990,8 +2995,7 @@ function useDataTableController({ path, collection, scrollRestoration, entitiesD
2990
2995
  dataClient,
2991
2996
  path,
2992
2997
  itemCount,
2993
- currentSort,
2994
- sortByProperty,
2998
+ sortKey,
2995
2999
  filterValues,
2996
3000
  searchString
2997
3001
  ]);
@@ -3067,9 +3071,9 @@ function useUpdateUrl(filterValues, sortBy, searchString, updateUrl) {
3067
3071
  }
3068
3072
  function encodeFilterAndSort(filterValues, sortBy) {
3069
3073
  const entries = {};
3070
- if (sortBy) {
3071
- entries["__sort"] = encodeURIComponent(sortBy[0]);
3072
- entries["__sort_order"] = encodeURIComponent(sortBy[1]);
3074
+ if (sortBy && sortBy.length > 0) {
3075
+ entries["__sort"] = sortBy.map(([field]) => encodeURIComponent(field)).join(",");
3076
+ entries["__sort_order"] = sortBy.map(([, direction]) => direction).join(",");
3073
3077
  }
3074
3078
  if (filterValues) Object.entries(filterValues).forEach(([key, value]) => {
3075
3079
  if (value) {
@@ -3105,8 +3109,11 @@ function parseFilterAndSort(search) {
3105
3109
  const filterValues = {};
3106
3110
  let sortBy = void 0;
3107
3111
  entries.forEach((value, key) => {
3108
- if (key === "__sort") sortBy = [decodeURIComponent(value), entries.get("__sort_order")];
3109
- else if (key.endsWith("_op")) {
3112
+ if (key === "__sort") {
3113
+ const directions = (entries.get("__sort_order") ?? "").split(",");
3114
+ const keys = value.split(",").map((field) => decodeURIComponent(field).trim()).filter(Boolean).map((field, index) => [field, directions[index] === "desc" ? "desc" : "asc"]);
3115
+ sortBy = keys.length > 0 ? keys : void 0;
3116
+ } else if (key.endsWith("_op")) {
3110
3117
  const field = key.replace("_op", "");
3111
3118
  const filterOp = decodeURIComponent(value);
3112
3119
  const filterValStr = entries.get(`${field}_value`);
@@ -3408,6 +3415,7 @@ var UIStyleGuide = () => {
3408
3415
  children: variant
3409
3416
  }), /* @__PURE__ */ jsxs(Typography, {
3410
3417
  variant,
3418
+ component: "p",
3411
3419
  children: [
3412
3420
  "The quick brown fox jumps over the lazy dog (",
3413
3421
  variant,
@@ -5899,10 +5907,10 @@ function CrmDashboardDemo() {
5899
5907
  children: [
5900
5908
  /* @__PURE__ */ jsxs("div", {
5901
5909
  className: "flex flex-col sm:flex-row sm:items-end justify-between mb-6 gap-4",
5902
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs(Typography, {
5910
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(Typography, {
5903
5911
  variant: "h4",
5904
5912
  className: "tracking-tight",
5905
- children: [getGreeting(), " 👋"]
5913
+ children: getGreeting()
5906
5914
  }), /* @__PURE__ */ jsxs(Typography, {
5907
5915
  variant: "body2",
5908
5916
  color: "secondary",
@@ -7800,6 +7808,7 @@ function UIReferenceView() {
7800
7808
  children: v
7801
7809
  }), /* @__PURE__ */ jsx(Typography, {
7802
7810
  variant: v,
7811
+ component: "p",
7803
7812
  children: "The quick brown fox jumps over the lazy dog"
7804
7813
  })]
7805
7814
  }, v))
@@ -9529,6 +9538,21 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9529
9538
  const [fadeIn, setFadeIn] = useState(false);
9530
9539
  const [viewVisible, setViewVisible] = useState(true);
9531
9540
  const [newsletterOptIn, setNewsletterOptIn] = useState(false);
9541
+ const authControllerRef = useRef(authController);
9542
+ authControllerRef.current = authController;
9543
+ /**
9544
+ * Subscribe the address that just authenticated, if the visitor asked for
9545
+ * it. Every sign-in path calls this, which is the point: the checkbox sits
9546
+ * beside the provider buttons, so it has to mean the same thing whichever
9547
+ * one is pressed.
9548
+ *
9549
+ * Only ever from a resolution path — a ticked box on a *failed* attempt
9550
+ * must not subscribe an address nobody proved they control.
9551
+ */
9552
+ const subscribeIfOptedIn = useCallback((email) => {
9553
+ if (!newsletterOptIn || !onNewsletterOptIn || !email) return;
9554
+ onNewsletterOptIn(email);
9555
+ }, [newsletterOptIn, onNewsletterOptIn]);
9532
9556
  const switchMode = (newMode) => {
9533
9557
  setViewVisible(false);
9534
9558
  setTimeout(() => {
@@ -9657,9 +9681,10 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9657
9681
  bootstrapMode: true,
9658
9682
  defaultEmail,
9659
9683
  defaultPassword,
9660
- onNewsletterOptIn,
9684
+ onSignedIn: subscribeIfOptedIn,
9661
9685
  newsletterOptIn,
9662
- setNewsletterOptIn
9686
+ setNewsletterOptIn,
9687
+ showNewsletterOptIn: Boolean(onNewsletterOptIn)
9663
9688
  }), !isBootstrapMode && /* @__PURE__ */ jsxs(Fragment, { children: [
9664
9689
  mode === "buttons" && /* @__PURE__ */ jsxs("div", {
9665
9690
  className: "w-full flex flex-col gap-3 mt-2",
@@ -9681,6 +9706,19 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9681
9706
  className: "w-full",
9682
9707
  children: topComponent
9683
9708
  }),
9709
+ onNewsletterOptIn && /* @__PURE__ */ jsxs("label", {
9710
+ className: cls("flex items-center gap-2 cursor-pointer", (topComponent || title || subtitle) && "-mt-3"),
9711
+ children: [/* @__PURE__ */ jsx(Checkbox, {
9712
+ checked: newsletterOptIn,
9713
+ onCheckedChange: (checked) => setNewsletterOptIn(checked === true),
9714
+ size: "small"
9715
+ }), /* @__PURE__ */ jsx(Typography, {
9716
+ variant: "caption",
9717
+ color: "secondary",
9718
+ className: "select-none",
9719
+ children: t("join_newsletter")
9720
+ })]
9721
+ }),
9684
9722
  /* @__PURE__ */ jsx(LoginButton, {
9685
9723
  disabled,
9686
9724
  text: "Sign in with email",
@@ -9690,7 +9728,8 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9690
9728
  hasGoogleLogin && googleClientId && /* @__PURE__ */ jsx(GoogleLoginButton, {
9691
9729
  disabled,
9692
9730
  googleClientId,
9693
- authController
9731
+ authController,
9732
+ onSignedIn: () => subscribeIfOptedIn(authControllerRef.current.user?.email)
9694
9733
  }),
9695
9734
  hasGitHubLogin && githubClientId && /* @__PURE__ */ jsx(GitHubLoginButton, {
9696
9735
  disabled,
@@ -9729,9 +9768,7 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9729
9768
  switchToRegister: showRegistration ? () => switchMode("register") : void 0,
9730
9769
  defaultEmail,
9731
9770
  defaultPassword,
9732
- onNewsletterOptIn,
9733
- newsletterOptIn,
9734
- setNewsletterOptIn
9771
+ onSignedIn: subscribeIfOptedIn
9735
9772
  }),
9736
9773
  mode === "register" && /* @__PURE__ */ jsx(LoginForm, {
9737
9774
  authController,
@@ -9743,9 +9780,7 @@ function LoginView({ logo, authController, noUserComponent, disableSignupScreen
9743
9780
  switchToLogin: () => switchMode("login"),
9744
9781
  defaultEmail,
9745
9782
  defaultPassword,
9746
- onNewsletterOptIn,
9747
- newsletterOptIn,
9748
- setNewsletterOptIn
9783
+ onSignedIn: subscribeIfOptedIn
9749
9784
  }),
9750
9785
  mode === "forgot" && authController.forgotPassword && /* @__PURE__ */ jsx(ForgotPasswordForm, {
9751
9786
  authController,
@@ -9803,8 +9838,10 @@ var GoogleIcon = () => /* @__PURE__ */ jsxs("svg", {
9803
9838
  })
9804
9839
  ]
9805
9840
  });
9806
- function GoogleLoginButton({ disabled, googleClientId, authController }) {
9841
+ function GoogleLoginButton({ disabled, googleClientId, authController, onSignedIn }) {
9807
9842
  const codeClientRef = useRef(null);
9843
+ const onSignedInRef = useRef(onSignedIn);
9844
+ onSignedInRef.current = onSignedIn;
9808
9845
  useEffect(() => {
9809
9846
  if (!authController.googleLogin) return;
9810
9847
  const google = window.google;
@@ -9823,6 +9860,7 @@ function GoogleLoginButton({ disabled, googleClientId, authController }) {
9823
9860
  code: response.code,
9824
9861
  redirectUri: "postmessage"
9825
9862
  });
9863
+ onSignedInRef.current?.();
9826
9864
  } catch (err) {
9827
9865
  console.error("Google login error:", err);
9828
9866
  }
@@ -9891,7 +9929,7 @@ function LinkedInLoginButton({ disabled, linkedinClientId }) {
9891
9929
  onClick: handleClick
9892
9930
  });
9893
9931
  }
9894
- function LoginForm({ onClose, onForgotPassword, authController, registrationMode, noUserComponent, disableSignupScreen, bootstrapMode = false, switchToRegister, switchToLogin, defaultEmail, defaultPassword, onNewsletterOptIn, newsletterOptIn = false, setNewsletterOptIn }) {
9932
+ function LoginForm({ onClose, onForgotPassword, authController, registrationMode, noUserComponent, disableSignupScreen, bootstrapMode = false, switchToRegister, switchToLogin, defaultEmail, defaultPassword, onSignedIn, newsletterOptIn = false, setNewsletterOptIn, showNewsletterOptIn = false }) {
9895
9933
  const passwordRef = useRef(null);
9896
9934
  const { t } = useTranslation();
9897
9935
  const emailId = useId();
@@ -9910,14 +9948,14 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
9910
9948
  document.removeEventListener("keydown", escFunction, false);
9911
9949
  };
9912
9950
  }, [onClose]);
9913
- function subscribeIfOptedIn(email) {
9914
- if (newsletterOptIn && onNewsletterOptIn) onNewsletterOptIn(email);
9951
+ function reportSignedIn(email) {
9952
+ onSignedIn?.(email);
9915
9953
  }
9916
9954
  function handleEnterPassword() {
9917
- if (email && password && authController.emailPasswordLogin) Promise.resolve(authController.emailPasswordLogin(email, password)).then(() => subscribeIfOptedIn(email)).catch(() => void 0);
9955
+ if (email && password && authController.emailPasswordLogin) Promise.resolve(authController.emailPasswordLogin(email, password)).then(() => reportSignedIn(email)).catch(() => void 0);
9918
9956
  }
9919
9957
  function handleRegistration() {
9920
- if (email && password && authController.register) Promise.resolve(authController.register(email, password, displayName)).then(() => subscribeIfOptedIn(email)).catch(() => void 0);
9958
+ if (email && password && authController.register) Promise.resolve(authController.register(email, password, displayName)).then(() => reportSignedIn(email)).catch(() => void 0);
9921
9959
  }
9922
9960
  const handleSubmit = (event) => {
9923
9961
  event.preventDefault();
@@ -10038,7 +10076,7 @@ function LoginForm({ onClose, onForgotPassword, authController, registrationMode
10038
10076
  children: "Forgot password?"
10039
10077
  })
10040
10078
  }),
10041
- onNewsletterOptIn && /* @__PURE__ */ jsxs("label", {
10079
+ showNewsletterOptIn && /* @__PURE__ */ jsxs("label", {
10042
10080
  className: "flex items-center gap-2 cursor-pointer mt-1 mb-1",
10043
10081
  children: [/* @__PURE__ */ jsx(Checkbox, {
10044
10082
  checked: newsletterOptIn,
@@ -10300,6 +10338,16 @@ var en = {
10300
10338
  clear_sort: "Clear sort",
10301
10339
  sort: "Sort",
10302
10340
  sort_by: "Sort by",
10341
+ sort_then_by: "Then by",
10342
+ sort_ascending: "Sort ascending",
10343
+ sort_descending: "Sort descending",
10344
+ sort_remove: "Remove sort",
10345
+ sort_move_up: "Make this key more important",
10346
+ sort_move_down: "Make this key less important",
10347
+ sort_remove_key: "Remove this sort key",
10348
+ sort_key_position: "Sort key {{position}}",
10349
+ sort_shift_click_hint: "Shift-click to add a column under the current sort",
10350
+ save_entity_before_subcollections: "You need to save your entity before adding additional collections",
10303
10351
  clear_all: "Clear all",
10304
10352
  no_items: "No items",
10305
10353
  no_entries_found: "No entries found",
@@ -11202,6 +11250,16 @@ var es = {
11202
11250
  clear_sort: "Borrar orden",
11203
11251
  sort: "Ordenar",
11204
11252
  sort_by: "Ordenar por",
11253
+ sort_then_by: "Luego por",
11254
+ sort_ascending: "Ordenar ascendente",
11255
+ sort_descending: "Ordenar descendente",
11256
+ sort_remove: "Quitar orden",
11257
+ sort_move_up: "Dar más importancia a esta clave",
11258
+ sort_move_down: "Dar menos importancia a esta clave",
11259
+ sort_remove_key: "Quitar esta clave de orden",
11260
+ sort_key_position: "Clave de orden {{position}}",
11261
+ sort_shift_click_hint: "Mayús+clic para añadir una columna bajo el orden actual",
11262
+ save_entity_before_subcollections: "Debes guardar la entidad antes de añadir colecciones adicionales",
11205
11263
  clear_all: "Borrar todo",
11206
11264
  no_items: "Sin elementos",
11207
11265
  no_entries_found: "No se encontraron entradas",
@@ -12060,6 +12118,16 @@ var de = {
12060
12118
  clear_sort: "Sortierung löschen",
12061
12119
  sort: "Sortieren",
12062
12120
  sort_by: "Sortieren nach",
12121
+ sort_then_by: "Dann nach",
12122
+ sort_ascending: "Aufsteigend sortieren",
12123
+ sort_descending: "Absteigend sortieren",
12124
+ sort_remove: "Sortierung entfernen",
12125
+ sort_move_up: "Diesen Schlüssel wichtiger machen",
12126
+ sort_move_down: "Diesen Schlüssel weniger wichtig machen",
12127
+ sort_remove_key: "Diesen Sortierschlüssel entfernen",
12128
+ sort_key_position: "Sortierschlüssel {{position}}",
12129
+ sort_shift_click_hint: "Umschalt+Klick, um eine Spalte unter der aktuellen Sortierung hinzuzufügen",
12130
+ save_entity_before_subcollections: "Du musst den Eintrag speichern, bevor du weitere Sammlungen hinzufügst",
12063
12131
  clear_all: "Alle löschen",
12064
12132
  no_items: "Keine Elemente",
12065
12133
  no_entries_found: "Keine Einträge gefunden",
@@ -12908,6 +12976,16 @@ var fr = {
12908
12976
  clear_sort: "Effacer le tri",
12909
12977
  sort: "Trier",
12910
12978
  sort_by: "Trier par",
12979
+ sort_then_by: "Puis par",
12980
+ sort_ascending: "Trier par ordre croissant",
12981
+ sort_descending: "Trier par ordre décroissant",
12982
+ sort_remove: "Supprimer le tri",
12983
+ sort_move_up: "Rendre cette clé plus importante",
12984
+ sort_move_down: "Rendre cette clé moins importante",
12985
+ sort_remove_key: "Supprimer cette clé de tri",
12986
+ sort_key_position: "Clé de tri {{position}}",
12987
+ sort_shift_click_hint: "Maj+clic pour ajouter une colonne sous le tri actuel",
12988
+ save_entity_before_subcollections: "Vous devez enregistrer l'entité avant d'ajouter des collections supplémentaires",
12911
12989
  clear_all: "Tout effacer",
12912
12990
  no_items: "Aucun élément",
12913
12991
  no_entries_found: "Aucune entrée trouvée",
@@ -13756,6 +13834,16 @@ var it = {
13756
13834
  clear_sort: "Cancella ordinamento",
13757
13835
  sort: "Ordina",
13758
13836
  sort_by: "Ordina per",
13837
+ sort_then_by: "Poi per",
13838
+ sort_ascending: "Ordina in modo crescente",
13839
+ sort_descending: "Ordina in modo decrescente",
13840
+ sort_remove: "Rimuovi ordinamento",
13841
+ sort_move_up: "Rendi questa chiave più importante",
13842
+ sort_move_down: "Rendi questa chiave meno importante",
13843
+ sort_remove_key: "Rimuovi questa chiave di ordinamento",
13844
+ sort_key_position: "Chiave di ordinamento {{position}}",
13845
+ sort_shift_click_hint: "Maiusc+clic per aggiungere una colonna sotto l'ordinamento attuale",
13846
+ save_entity_before_subcollections: "Devi salvare l'entità prima di aggiungere altre raccolte",
13759
13847
  clear_all: "Cancella tutto",
13760
13848
  no_items: "Nessun elemento",
13761
13849
  no_entries_found: "Nessuna voce trovata",
@@ -14604,6 +14692,16 @@ var hi = {
14604
14692
  clear_sort: "सॉर्ट साफ़ करें",
14605
14693
  sort: "क्रमबद्ध करें",
14606
14694
  sort_by: "इसके अनुसार क्रमबद्ध करें",
14695
+ sort_then_by: "फिर इसके अनुसार",
14696
+ sort_ascending: "आरोही क्रम में लगाएँ",
14697
+ sort_descending: "अवरोही क्रम में लगाएँ",
14698
+ sort_remove: "क्रम हटाएँ",
14699
+ sort_move_up: "इस कुंजी को अधिक महत्व दें",
14700
+ sort_move_down: "इस कुंजी को कम महत्व दें",
14701
+ sort_remove_key: "यह क्रम कुंजी हटाएँ",
14702
+ sort_key_position: "क्रम कुंजी {{position}}",
14703
+ sort_shift_click_hint: "वर्तमान क्रम के नीचे कॉलम जोड़ने के लिए Shift+क्लिक करें",
14704
+ save_entity_before_subcollections: "अतिरिक्त संग्रह जोड़ने से पहले आपको एंटिटी सहेजनी होगी",
14607
14705
  clear_all: "सभी साफ़ करें",
14608
14706
  no_items: "कोई आइटम नहीं",
14609
14707
  no_entries_found: "कोई प्रविष्टि नहीं मिली",
@@ -15457,6 +15555,16 @@ var pt = {
15457
15555
  clear_sort: "Limpar ordenação",
15458
15556
  sort: "Ordenar",
15459
15557
  sort_by: "Ordenar por",
15558
+ sort_then_by: "Depois por",
15559
+ sort_ascending: "Ordenar ascendente",
15560
+ sort_descending: "Ordenar descendente",
15561
+ sort_remove: "Remover ordenação",
15562
+ sort_move_up: "Tornar esta chave mais importante",
15563
+ sort_move_down: "Tornar esta chave menos importante",
15564
+ sort_remove_key: "Remover esta chave de ordenação",
15565
+ sort_key_position: "Chave de ordenação {{position}}",
15566
+ sort_shift_click_hint: "Shift+clique para adicionar uma coluna sob a ordenação atual",
15567
+ save_entity_before_subcollections: "Precisas de guardar a entidade antes de adicionar coleções adicionais",
15460
15568
  clear_all: "Limpar tudo",
15461
15569
  no_items: "Sem itens",
15462
15570
  no_entries_found: "Nenhum registo encontrado",