Skip to content

Commit

Permalink
Merge pull request #833 from wri/feat/TM-1641-spa
Browse files Browse the repository at this point in the history
[TM-1641] Transition toward single page app
  • Loading branch information
roguenet authored Jan 27, 2025
2 parents 9a28cf6 + f6a1ff5 commit faa026a
Show file tree
Hide file tree
Showing 52 changed files with 947 additions and 1,184 deletions.
9 changes: 4 additions & 5 deletions .env.local.sample
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Valid values are "local", "dev", "test", "staging", "prod"
NEXT_PUBLIC_TARGET_ENV = "local"

# Get these two values securely from another dev or from Transifex directly.
TRANSIFEX_TOKEN =
TRANSIFEX_SECRET =
TRANSIFEX_TRANSLATIONS_TTL_SEC = 10
NEXT_PUBLIC_TRANSIFEX_TOKEN =

IMAGE_DOMAINS = 'localhost,wri.s3.us-west-2.amazonaws.com,s3-eu-west-1.amazonaws.com'

# Get this value securely from another dev or from Transifex directly.
TRANSIFEX_SECRET =
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Pushing new translations to the Transifex cloud involves just one manual step. Y

For more details about the command-line interface (CLI), please refer to the [official documentation](https://developers.transifex.com/docs/cli).

It's important to note that the setup requires the correct configuration of `TRANSIFEX_TOKEN` and `TRANSIFEX_SECRET` environment variables. Due to the sensitive nature of these variables, they will be securely shared with you via 1Password.
It's important to note that the setup requires the correct configuration of `NEXT_PUBLIC_TRANSIFEX_TOKEN` and `TRANSIFEX_SECRET` environment variables. Due to the sensitive nature of these variables, they will be securely shared with you via 1Password.

Additionally, consider automating this step using GitHub Actions for increased efficiency.

Expand Down
3 changes: 0 additions & 3 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ const nextConfig = {
//Added "page.tsx", "page.ts" to get middleware.page.ts working
// https://github.com/vercel/next.js/issues/38233#issuecomment-1172457237
pageExtensions: ["tsx", "page.tsx", "page.ts"],
publicRuntimeConfig: {
TxNativePublicToken: process.env.TRANSIFEX_TOKEN
},
images: { domains: process.env.IMAGE_DOMAINS?.split(",") ?? ["s3-eu-west-1.amazonaws.com"] },
// webpack5: true,
webpack(config) {
Expand Down
5 changes: 4 additions & 1 deletion openapi-codegen.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,10 @@ const createPredicateNodes = ({
undefined,
f.createIdentifier("variables"),
undefined,
variablesType,
f.createTypeReferenceNode("Omit", [
variablesType,
f.createLiteralTypeNode(f.createStringLiteral("body"))
]),
undefined
)
],
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"postinstall": "patch-package",
"dev": "next dev",
"build": "npm run generate:api && npm run generate:services && next build",
"build": "yarn generate:api && yarn generate:services && next build",
"start": "next start",
"test": "jest --watch",
"test:coverage": "jest --coverage",
Expand All @@ -20,8 +20,8 @@
"generate:userService": "openapi-codegen gen userService",
"generate:entityService": "openapi-codegen gen entityService",
"generate:services": "yarn generate:userService && yarn generate:entityService && yarn generate:jobService",
"tx:push": "eval $(grep '^TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli push --key-generator=hash src/ --token=$TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET",
"tx:pull": "eval $(grep '^TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli pull --token=$TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET"
"tx:push": "eval $(grep '^NEXT_PUBLIC_TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli push --key-generator=hash src/ --token=$NEXT_PUBLIC_TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET",
"tx:pull": "eval $(grep '^NEXT_PUBLIC_TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli pull --token=$NEXT_PUBLIC_TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET"
},
"dependencies": {
"@faker-js/faker": "^8.0.2",
Expand Down Expand Up @@ -62,9 +62,9 @@
"mapbox-gl": "^2.15.0",
"mapbox-gl-draw-circle": "^1.1.2",
"next": "13.1.5",
"next-redux-wrapper": "^8.1.0",
"nookies": "^2.5.2",
"prettier-plugin-tailwindcss": "^0.2.2",
"query-string": "^7.1.1",
"ra-input-rich-text": "^4.12.2",
"react": "18.2.0",
"react-admin": "^4.7.4",
Expand All @@ -76,9 +76,10 @@
"react-inlinesvg": "^3.0.0",
"react-joyride": "^2.5.5",
"react-redux": "^9.1.2",
"redux-logger": "^3.0.6",
"recharts": "^2.13.0",
"redux-logger": "^3.0.6",
"reselect": "^4.1.8",
"sharp": "^0.33.5",
"swiper": "^9.0.5",
"tailwind-merge": "^1.14.0",
"typescript": "4.9.4",
Expand Down
5 changes: 2 additions & 3 deletions src/admin/apiProvider/authProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AuthProvider } from "react-admin";

import { isAdmin, UserRole } from "@/admin/apiProvider/utils/user";
import { loadLogin, logout } from "@/connections/Login";
import { loadMyUser } from "@/connections/User";
import Log from "@/utils/log";
Expand All @@ -14,10 +13,10 @@ export const authProvider: AuthProvider = {

// when the user navigates, make sure that their credentials are still valid
checkAuth: async () => {
const { user } = await loadMyUser();
const { user, isAdmin } = await loadMyUser();
if (user == null) throw "No user logged in.";

if (!isAdmin(user.primaryRole as UserRole)) throw "Only admins are allowed.";
if (!isAdmin) throw "Only admins are allowed.";
},

// remove local credentials
Expand Down
12 changes: 0 additions & 12 deletions src/admin/apiProvider/utils/user.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/admin/components/ResourceTabs/GalleryTab/GalleryTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from "@transifex/native";
import { useT } from "@transifex/react";
import { FC, useEffect, useState } from "react";
import { TabbedShowLayout, TabProps, useShowContext } from "react-admin";
import { When } from "react-if";
Expand All @@ -22,6 +22,7 @@ interface IProps extends Omit<TabProps, "label" | "children"> {
}

const GalleryTab: FC<IProps> = ({ label, entity, ...rest }) => {
const t = useT();
const ctx = useShowContext();
const [pagination, setPagination] = useState({ page: 1, pageSize: 10 });
const [filter] = useState<string>("all");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,53 @@
import { Popover } from "@headlessui/react";
import { useT } from "@transifex/react";
import classNames from "classnames";
import { useRouter } from "next/router";
import { PropsWithChildren, useRef, useState } from "react";

import Text from "@/components/elements/Text/Text";
import Icon, { IconNames } from "@/components/extensive/Icon/Icon";
import List from "@/components/extensive/List/List";
import { fetchPatchV2UsersLocale } from "@/generated/apiComponents";
import { useValueChanged } from "@/hooks/useValueChanged";
import { Option, OptionValue } from "@/types/common";

export interface DropdownProps {
defaultValue?: OptionValue;
onChange?: (value: OptionValue) => void;
onChange?: (value: string) => void;
className?: string;
isLoggedIn?: boolean;
}

const LANGUAGES: Option[] = [
{ value: "en-US", title: "English" },
{ value: "es-MX", title: "Spanish" },
{ value: "fr-FR", title: "French" },
{ value: "pt-BR", title: "Portuguese" }
];

const languageForLocale = (locale?: string | null) => LANGUAGES.find(({ value }) => value === locale) ?? LANGUAGES[0];

const LanguagesDropdown = (props: PropsWithChildren<DropdownProps>) => {
const t = useT();
const Languages: Option[] = [
{ value: "en-US", title: t("English") },
{ value: "es-MX", title: t("Spanish") },
{ value: "fr-FR", title: t("French") },
{ value: "pt-BR", title: t("Portuguese") }
];
const router = useRouter();

const [selected, setSelected] = useState<Option>(Languages[0]);
const [selected, setSelected] = useState<Option>(languageForLocale(router.locale));
let buttonRef = useRef<any>();

useValueChanged(router.locale, () => {
setSelected(languageForLocale(router.locale));
});

const onChange = (lang: Option) => {
setSelected(lang);
props.onChange?.(lang.value);
props.onChange?.(lang.value as string);
buttonRef.current?.click();
if (props.isLoggedIn) {
fetchPatchV2UsersLocale({
body: { locale: lang.value as string }
});
}
};

return (
<Popover className={classNames(props.className, "relative w-fit")}>
<Popover.Button ref={buttonRef} className="flex items-center justify-between p-2">
<Icon name={IconNames.EARTH} width={16} className="mr-2 fill-neutral-700" />
<span className="text-14-light mr-2 whitespace-nowrap text-sm uppercase text-darkCustom">
{selected?.title}
{t(selected?.title)}
</span>
<Icon
name={IconNames.TRIANGLE_DOWN}
Expand All @@ -55,14 +58,14 @@ const LanguagesDropdown = (props: PropsWithChildren<DropdownProps>) => {

<Popover.Panel className="border-1 absolute right-0 z-50 mt-4 w-[130px] border border-neutral-300 bg-white shadow">
<List
items={Languages}
items={LANGUAGES}
render={item => (
<Text
variant={selected.value === item.value ? "text-body-900" : "text-body-600"}
className={classNames("px-3 py-1 uppercase text-neutral-900 first:pt-2 last:pb-2 hover:bg-neutral-200")}
onClick={() => onChange(item)}
>
{item.title}
{t(item.title)}
</Text>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from "@transifex/native";
import { useT } from "@transifex/react";
import classNames from "classnames";
import { Dispatch, SetStateAction } from "react";

Expand All @@ -18,6 +18,7 @@ const ImageControl = ({
viewImages: boolean;
setViewImages: Dispatch<SetStateAction<boolean>>;
}) => {
const t = useT();
const { openModal, closeModal } = useModalContext();

const openFormModalHandlerUploadImages = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from "@transifex/native";
import { useT } from "@transifex/react";
import { useEffect, useMemo, useState } from "react";

import Icon, { IconNames } from "@/components/extensive/Icon/Icon";
Expand All @@ -15,6 +15,7 @@ const ViewImageCarousel = ({
modelFilesData: GetV2MODELUUIDFilesResponse["data"];
imageGalleryRef?: React.RefObject<HTMLDivElement>;
}) => {
const t = useT();
const modelFilesTabItems: TabImagesItem[] = useMemo(() => {
const modelFilesGeolocalized: GetV2MODELUUIDFilesResponse["data"] = [];
const modelFilesNonGeolocalized: GetV2MODELUUIDFilesResponse["data"] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { t } from "@transifex/native";
import { useT } from "@transifex/react";
import classNames from "classnames";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { When } from "react-if";
Expand Down Expand Up @@ -34,6 +34,7 @@ const MapEditPolygonPanel = ({
polygonData,
recallEntityData
}: MapEditPolygonPanelProps) => {
const t = useT();
const {
editPolygon,
setEditPolygon,
Expand Down
4 changes: 3 additions & 1 deletion src/components/elements/Table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ function Table<TData extends RowData>({
onTableStateChange?.({ sorting, filters });
},
getRowId: (row: any) => row.uuid,
debugTable: process.env.NODE_ENV === "development",

// Uncomment for local debug testing fo the table.
// debugTable: process.env.NODE_ENV === "development",

autoResetAll: resetOnDataChange
});
Expand Down
31 changes: 14 additions & 17 deletions src/components/generic/Navbar/NavbarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import { Else, If, Then, When } from "react-if";
import LanguagesDropdown from "@/components/elements/Inputs/LanguageDropdown/LanguagesDropdown";
import { IconNames } from "@/components/extensive/Icon/Icon";
import List from "@/components/extensive/List/List";
import { useLogin } from "@/connections/Login";
import { logout, useLogin } from "@/connections/Login";
import { useMyOrg } from "@/connections/Organisation";
import { useMyUser, ValidLocale } from "@/connections/User";
import { useNavbarContext } from "@/context/navbar.provider";
import { useLogout } from "@/hooks/logout";
import { OptionValue } from "@/types/common";

import NavbarItem from "./NavbarItem";
import { getNavbarItems } from "./navbarItems";
Expand All @@ -24,27 +23,25 @@ const NavbarContent = ({ handleClose, ...rest }: NavbarContentProps) => {
const [, { isLoggedIn }] = useLogin();
const router = useRouter();
const t = useT();
const [, { setLocale }] = useMyUser();
const [, myOrg] = useMyOrg();
const logout = useLogout();
const { private: privateNavItems, public: publicNavItems } = getNavbarItems(t, myOrg);

const navItems = (isLoggedIn ? privateNavItems : publicNavItems).filter(item => item.visibility);

const { linksDisabled } = useNavbarContext();

const setV1Lang = (lang: string) => {
let v1Lang = lang;
const changeLanguageHandler = (lang: string) => {
if (setLocale != null) {
// In this case, the Bootstrap component will notice the changed user locale and update our URL for us
// after the server round trip. We don't want to do it here because then it's a race condition
// that can cause the URL locale to flicker.
setLocale(lang as ValidLocale);
} else {
// In this case we don't have a user to store the locale on, so just go ahead and directly change the URL.
router.push({ pathname: router.pathname, query: router.query }, router.asPath, { locale: lang.toString() });
}

if (lang === "es-MX") v1Lang = "es";
if (lang === "fr-FR") v1Lang = "fr";

localStorage.setItem("i18nextLng", v1Lang);
};

const changeLanguageHandler = (lang: OptionValue) => {
//Change Locale without changing the route
router.push({ pathname: router.pathname, query: router.query }, router.asPath, { locale: lang.toString() });
setV1Lang(lang as string);
handleClose?.();
};

Expand Down Expand Up @@ -84,7 +81,7 @@ const NavbarContent = ({ handleClose, ...rest }: NavbarContentProps) => {
</NavbarItem>
</Else>
</If>
<LanguagesDropdown onChange={changeLanguageHandler} isLoggedIn={isLoggedIn} className="hidden sm:block" />
<LanguagesDropdown onChange={changeLanguageHandler} className="hidden sm:block" />
</div>
);
};
Expand Down
4 changes: 1 addition & 3 deletions src/components/generic/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import { MENU_PLACEMENT_RIGHT_TOP } from "@/components/elements/Menu/MenuVariant
import Text from "@/components/elements/Text/Text";
import Tooltip from "@/components/elements/Tooltip/Tooltip";
import Icon, { IconNames } from "@/components/extensive/Icon/Icon";
import { useLogin } from "@/connections/Login";
import { useLogout } from "@/hooks/logout";
import { logout, useLogin } from "@/connections/Login";

const Sidebar = () => {
const router = useRouter();
const logout = useLogout();
const [, { isLoggedIn }] = useLogin();

const t = useT();
Expand Down
3 changes: 2 additions & 1 deletion src/connections/Login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const logout = () => {
// When we log out, remove all cached API resources so that when we log in again, these resources
// are freshly fetched from the BE.
ApiSlice.clearApiCache();
window.location.replace("/auth/login");
ApiSlice.queryClient?.getQueryCache()?.clear();
ApiSlice.queryClient?.clear();
};

export const selectFirstLogin = (store: ApiDataStore) => Object.values(store.logins)?.[0]?.attributes;
Expand Down
Loading

0 comments on commit faa026a

Please sign in to comment.