[FEATURE] New Project Display #1
18 changed files with 247 additions and 45 deletions
|
@ -31,4 +31,8 @@ export default class ProjectTableService extends TableService {
|
||||||
throw ProjectErrors.ConflictNonUnique;
|
throw ProjectErrors.ConflictNonUnique;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listChildren(parentProjectId: string) {
|
||||||
|
return this.pg.project.findMany({ where: { parentProject: parentProjectId } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ type CreateCRC = ContractRouteContext<{
|
||||||
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type ListCRC = ContractRouteContext<{ RequestParamsContract: typeof ProjectContract.ProjectParams }>;
|
||||||
|
|
||||||
export default class ProjectController extends VixpressController {
|
export default class ProjectController extends VixpressController {
|
||||||
declare pg: PostgresService;
|
declare pg: PostgresService;
|
||||||
constructor(app: Express) {
|
constructor(app: Express) {
|
||||||
|
@ -45,4 +47,13 @@ export default class ProjectController extends VixpressController {
|
||||||
const publicKey = await decrypt(kp.encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
const publicKey = await decrypt(kp.encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||||
return { user: usr, project: proj, publicKey };
|
return { user: usr, project: proj, publicKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async list(crc: any): Promise<CProjectContract["ListProjects"]> {
|
||||||
|
const { project: parentProject } = crc.req as UserRequest;
|
||||||
|
const projectPromise = this.pg.project.byId(parentProject.id);
|
||||||
|
const childProjectsPromise = this.pg.project.listChildren(parentProject.id);
|
||||||
|
const [project, childProjects] = await Promise.all([projectPromise, childProjectsPromise]);
|
||||||
|
if (!project) throw ProjectErrors.NotFoundProject;
|
||||||
|
return { project, childProjects };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,11 @@ export class ProjectRoute extends VixpressRoute {
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const projCreate = { ...cBase, reqBody: ProjectContract.Create };
|
const projCreate = { ...cBase, reqBody: ProjectContract.Create };
|
||||||
|
const projList = { reqParams: ProjectContract.ProjectParams, resBody: ProjectContract.ListProjects };
|
||||||
// Middleware
|
// Middleware
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
router.post("/create", RouteGuard.MangeProjectsCreate, contract(projController.create, projCreate));
|
router.post("/create", RouteGuard.MangeProjectsCreate, contract(projController.create, projCreate));
|
||||||
|
router.get("/list", RouteGuard.ManageProjectsRead, contract(projController.list, projList));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,30 @@ import * as y from "yup";
|
||||||
import { DatabaseContract } from "./database.contracts";
|
import { DatabaseContract } from "./database.contracts";
|
||||||
// ====================================== Reused Contracts ======================================
|
// ====================================== Reused Contracts ======================================
|
||||||
|
|
||||||
|
const Project = y.object({
|
||||||
|
id: y.string().required(),
|
||||||
|
slug: y.string().required(),
|
||||||
|
parentProject: y.string().required(),
|
||||||
|
name: y.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
// ====================================== Response Contracts ======================================
|
// ====================================== Response Contracts ======================================
|
||||||
|
|
||||||
export const ProjectContractRes = defineContractExport("CProjectContractRes", {
|
export const ProjectContractRes = defineContractExport("CProjectContractRes", {
|
||||||
CreateResponse: y.object({
|
CreateResponse: y
|
||||||
|
.object({
|
||||||
project: DatabaseContract.Project,
|
project: DatabaseContract.Project,
|
||||||
user: DatabaseContract.User,
|
user: DatabaseContract.User,
|
||||||
publicKey: y.string().required(),
|
publicKey: y.string().required(),
|
||||||
}),
|
})
|
||||||
|
.required(),
|
||||||
|
Project: Project.required(),
|
||||||
|
ListProjects: y
|
||||||
|
.object({
|
||||||
|
project: Project.required(),
|
||||||
|
childProjects: y.array(Project).required(),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ====================================== Request Contracts ======================================
|
// ====================================== Request Contracts ======================================
|
||||||
|
|
|
@ -18,6 +18,7 @@ export class ProjectErrors {
|
||||||
static readonly BadRequestProjectIncomplete = new ClientError(400, "Project incomplete!");
|
static readonly BadRequestProjectIncomplete = new ClientError(400, "Project incomplete!");
|
||||||
static readonly UnexpectedRootUserError = new ClientError(500, "Error creating root user!");
|
static readonly UnexpectedRootUserError = new ClientError(500, "Error creating root user!");
|
||||||
static readonly ConflictNonUnique = new ClientError(409, "Slug already taken!");
|
static readonly ConflictNonUnique = new ClientError(409, "Slug already taken!");
|
||||||
|
static readonly NotFoundProject = new ClientError(404, "Project not found!");
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeyPairErrors {
|
export class KeyPairErrors {
|
||||||
|
|
29
package-lock.json
generated
29
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "cairo",
|
"name": "cairo",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cairo",
|
"name": "cairo",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"license": "LGPL-2.1",
|
"license": "LGPL-2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
"@prisma/client": "^5.18.0",
|
"@prisma/client": "^5.18.0",
|
||||||
"@sendgrid/mail": "^8.1.3",
|
"@sendgrid/mail": "^8.1.3",
|
||||||
|
"@tanstack/react-query": "^5.53.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cron": "^3.1.7",
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
@ -2709,6 +2710,30 @@
|
||||||
"node": ">=12.*"
|
"node": ">=12.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.53.3.tgz",
|
||||||
|
"integrity": "sha512-ZfjAgd7NpqDx0e4aYBt7EmS2enbulPrJwowTy+mayRE93WUUH+sIYHun1TdRjpGwDPMNNZ5D6goh7n3CwoO+HA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.53.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.53.3.tgz",
|
||||||
|
"integrity": "sha512-286mN/91CeM7vC6CZFLKYDHSw+WyMX6ekIvzoTbpM4xyPb99VSyCKPLyPgaOatKqYm6ooMBquSq9NGRdKgsJfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.53.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
|
@ -45,13 +45,14 @@
|
||||||
"vite-tsconfig-paths": "^5.0.1"
|
"vite-tsconfig-paths": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dunemask/vix": "^0.0.1-alpha.0",
|
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
"@dunemask/vix": "^0.0.1-alpha.0",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@emotion/styled": "^11.13.0",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
"@prisma/client": "^5.18.0",
|
"@prisma/client": "^5.18.0",
|
||||||
"@sendgrid/mail": "^8.1.3",
|
"@sendgrid/mail": "^8.1.3",
|
||||||
|
"@tanstack/react-query": "^5.53.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cron": "^3.1.7",
|
"cron": "^3.1.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|
|
@ -4,18 +4,25 @@ import { ChakraProvider } from "@chakra-ui/react";
|
||||||
import useInitHooks from "@src/hooks/init-hooks";
|
import useInitHooks from "@src/hooks/init-hooks";
|
||||||
import theme from "@src/util/theme";
|
import theme from "@src/util/theme";
|
||||||
import { AuthProvider } from "@src/ctx/AuthContext";
|
import { AuthProvider } from "@src/ctx/AuthContext";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
|
||||||
import Viewport from "./Viewport";
|
import Viewport from "./Viewport";
|
||||||
|
import { ProjectProvider } from "./ctx/ProjectContext";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const qc = new QueryClient();
|
||||||
return (
|
return (
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<ProjectProvider>
|
||||||
<InitProvider>
|
<InitProvider>
|
||||||
<Viewport />
|
<Viewport />
|
||||||
</InitProvider>
|
</InitProvider>
|
||||||
|
</ProjectProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ declare type Portal = { path: Links; view: ReactNode };
|
||||||
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
|
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
|
||||||
const { auth, loading, initialized } = useAuth();
|
const { auth, loading, initialized } = useAuth();
|
||||||
const Component = props.view;
|
const Component = props.view;
|
||||||
if (!loading && !initialized && !!auth) return <Component />;
|
if (!loading && initialized && !!auth) return <Component />;
|
||||||
return <AutoRedirect />;
|
return <AutoRedirect />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/components/projects/ProjectTile.tsx
Normal file
16
src/components/projects/ProjectTile.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Box, Flex, Skeleton, Text } from "@chakra-ui/react";
|
||||||
|
import { CProjectContract } from "@lib/contracts/project.contracts";
|
||||||
|
|
||||||
|
export default function ProjectTile(props: { project: CProjectContract["Project"] }) {
|
||||||
|
return (
|
||||||
|
<Box w="210px" h="210px" bg="background.paper" _hover={{ opacity: ".9", cursor: "pointer" }} borderRadius=".5rem">
|
||||||
|
<Text>{props.project.name ?? props.project.slug}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectTileSkeleton() {
|
||||||
|
return (
|
||||||
|
<Skeleton w="210px" h="210px" bg="background.paper" _hover={{ opacity: ".9", cursor: "pointer" }} borderRadius=".5rem" />
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,8 +4,8 @@ import { Policy, PolicyString } from "@lib/Policies";
|
||||||
import { Resource } from "@lib/vix/AppResources";
|
import { Resource } from "@lib/vix/AppResources";
|
||||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { PROJECT_CONTEXT_INITIAL_STATE } from "./ProjectContext";
|
||||||
|
|
||||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
|
||||||
const credentialApiPath = `auth/credentials`;
|
const credentialApiPath = `auth/credentials`;
|
||||||
|
|
||||||
export enum AuthStorageKeys {
|
export enum AuthStorageKeys {
|
||||||
|
@ -154,7 +154,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
|
||||||
async function getPolicies(token: string): Promise<Policy[]> {
|
async function getPolicies(token: string): Promise<Policy[]> {
|
||||||
const extraHeaders = { Authorization: `Bearer ${token}` };
|
const extraHeaders = { Authorization: `Bearer ${token}` };
|
||||||
const projectId = !!search.get("projectId") ? search.get("projectId") : project;
|
const projectId = !!search.get("projectId") ? search.get("projectId") : PROJECT_CONTEXT_INITIAL_STATE.projectId;
|
||||||
const apiPath = `/${projectId}/${credentialApiPath}`;
|
const apiPath = `/${projectId}/${credentialApiPath}`;
|
||||||
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
|
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
|
||||||
if (!credentials) return [];
|
if (!credentials) return [];
|
||||||
|
@ -183,7 +183,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
|
||||||
async function fetchCredentials(token: string): Promise<[user: CDatabaseContract["User"], rp: Policy[]]> {
|
async function fetchCredentials(token: string): Promise<[user: CDatabaseContract["User"], rp: Policy[]]> {
|
||||||
const extraHeaders = getAuthHeader(token);
|
const extraHeaders = getAuthHeader(token);
|
||||||
const projectId = !!search.get("projectId") ? search.get("projectId") : project;
|
const projectId = !!search.get("projectId") ? search.get("projectId") : PROJECT_CONTEXT_INITIAL_STATE.projectId;
|
||||||
const apiPath = `/${projectId}/${credentialApiPath}`;
|
const apiPath = `/${projectId}/${credentialApiPath}`;
|
||||||
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
|
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
|
||||||
if (!credentials) throw Error("Could not authenticate!");
|
if (!credentials) throw Error("Could not authenticate!");
|
||||||
|
|
29
src/ctx/ProjectContext.tsx
Normal file
29
src/ctx/ProjectContext.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { createContext, ReactNode, useContext, useState } from "react";
|
||||||
|
|
||||||
|
interface ProjectState {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectContextType extends ProjectState {
|
||||||
|
setProjectId: (v: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PROJECT_CONTEXT_INITIAL_STATE: ProjectContextType = {
|
||||||
|
projectId: import.meta.env.VITE_CAIRO_PROJECT as string,
|
||||||
|
setProjectId: () => void 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectContext = createContext<ProjectContextType>(PROJECT_CONTEXT_INITIAL_STATE);
|
||||||
|
|
||||||
|
export const useProjectContext = () => useContext<ProjectContextType>(ProjectContext);
|
||||||
|
|
||||||
|
export const ProjectProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [projectId, setProjectId] = useState<string>(PROJECT_CONTEXT_INITIAL_STATE.projectId);
|
||||||
|
|
||||||
|
const context: ProjectContextType = {
|
||||||
|
projectId,
|
||||||
|
setProjectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ProjectContext.Provider value={context}>{children}</ProjectContext.Provider>;
|
||||||
|
};
|
|
@ -1,9 +1,37 @@
|
||||||
import { useAuth } from "@src/ctx/AuthContext";
|
import { useAuth } from "@src/ctx/AuthContext";
|
||||||
|
import { PROJECT_CONTEXT_INITIAL_STATE, useProjectContext } from "@src/ctx/ProjectContext";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function useInitHooks() {
|
export default function useInitHooks() {
|
||||||
const { authInit } = useAuth();
|
const { authInit, auth } = useAuth();
|
||||||
|
const { setProjectId } = useProjectContext();
|
||||||
|
const [search] = useSearchParams();
|
||||||
|
|
||||||
useEffect(function initHooks() {
|
useEffect(function initHooks() {
|
||||||
authInit();
|
authInit();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function authInitHooks() {
|
||||||
|
if (!auth) return;
|
||||||
|
},
|
||||||
|
[auth],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function paramHooks() {
|
||||||
|
projectIdParamHook();
|
||||||
|
},
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
async function nonAuthInitHooks() {}
|
||||||
|
|
||||||
|
async function authInitHooks() {}
|
||||||
|
|
||||||
|
function projectIdParamHook() {
|
||||||
|
const projectId = !!search.get("projectId") ? (search.get("projectId") as string) : PROJECT_CONTEXT_INITIAL_STATE.projectId;
|
||||||
|
setProjectId(projectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,13 @@ export const getProjectAuthVerify = (project: string) =>
|
||||||
authenticatedApiRequest({subpath: `/${project}/auth/verify`, method:"GET", jsonify: true});
|
authenticatedApiRequest({subpath: `/${project}/auth/verify`, method:"GET", jsonify: true});
|
||||||
|
|
||||||
export const postProjectAuthLogin = (project: string,login: CAuthContract["Login"]) =>
|
export const postProjectAuthLogin = (project: string,login: CAuthContract["Login"]) =>
|
||||||
apiRequest<CAuthContract["LoginCredentials"]>({
|
apiRequest<CAuthContract["LoginCredentials"]>({subpath: `/${project}/auth/login`, method:"POST", json: login, jsonify: true});
|
||||||
subpath: `/${project}/auth/login`,
|
|
||||||
method: "POST",
|
|
||||||
json: login,
|
|
||||||
jsonify: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getProjectAuthCredentials = (project: string) =>
|
export const getProjectAuthCredentials = (project: string) =>
|
||||||
authenticatedApiRequest<CAuthContract["Credentials"]>({
|
authenticatedApiRequest<CAuthContract["Credentials"]>({subpath: `/${project}/auth/credentials`, method:"GET", jsonify: true});
|
||||||
subpath: `/${project}/auth/credentials`,
|
|
||||||
method: "GET",
|
export const postProjectCreate = (project: string,create: CProjectContract["Create"]) =>
|
||||||
jsonify: true,
|
authenticatedApiRequest({subpath: `/${project}/create`, method:"POST", json: create, jsonify: true});
|
||||||
});
|
|
||||||
|
export const getProjectList = (project: string) =>
|
||||||
|
authenticatedApiRequest<CProjectContract["ListProjects"]>({subpath: `/${project}/list`, method:"GET", jsonify: true});
|
15
src/util/api/queries.ts
Normal file
15
src/util/api/queries.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getProjectList } from "./GeneratedRequests";
|
||||||
|
|
||||||
|
export enum QueryKeys {
|
||||||
|
ProjectList = "ProjectList",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMutator = (apiRequest: (...args: any) => Promise<any>, invalidate: string[]) => {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const mutate = async (...args: any) => apiRequest(...args).then(() => qc.invalidateQueries({ queryKey: invalidate }));
|
||||||
|
return mutate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectList = (projectId: string) =>
|
||||||
|
useQuery({ queryKey: [QueryKeys.ProjectList], queryFn: () => getProjectList(projectId) });
|
|
@ -1,9 +1,9 @@
|
||||||
import { apiRequest, ApiRequestArgs } from "@dunemask/vix/bridge";
|
import { apiRequest, ApiRequestArgs } from "@dunemask/vix/bridge";
|
||||||
import { getAuthHeader } from "@src/ctx/AuthContext";
|
import { getActiveUserToken, getAuthHeader } from "@src/ctx/AuthContext";
|
||||||
|
|
||||||
export async function authenticatedApiRequest<K = any>(apiRequestArgs: ApiRequestArgs): Promise<K> {
|
export async function authenticatedApiRequest<K = any>(apiRequestArgs: ApiRequestArgs): Promise<K> {
|
||||||
const extraHeaders = apiRequestArgs.extraHeaders ?? {};
|
const extraHeaders = apiRequestArgs.extraHeaders ?? {};
|
||||||
const authHeaders = getAuthHeader();
|
const authHeaders = getAuthHeader(getActiveUserToken());
|
||||||
apiRequestArgs.extraHeaders = { ...extraHeaders, ...authHeaders };
|
apiRequestArgs.extraHeaders = { ...extraHeaders, ...authHeaders };
|
||||||
return apiRequest(apiRequestArgs);
|
return apiRequest(apiRequestArgs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,15 @@ import { PasswordInput } from "@src/components/common/Inputs";
|
||||||
import { ResourcePolicyType } from "@dunemask/vix/util";
|
import { ResourcePolicyType } from "@dunemask/vix/util";
|
||||||
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
|
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
|
||||||
import { Navigate, useSearchParams } from "react-router-dom";
|
import { Navigate, useSearchParams } from "react-router-dom";
|
||||||
|
import { useProjectContext } from "@src/ctx/ProjectContext";
|
||||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
|
||||||
|
|
||||||
export default function AuthenticateView() {
|
export default function AuthenticateView() {
|
||||||
const { auth, login } = useAuth();
|
const { auth, login } = useAuth();
|
||||||
|
const { projectId } = useProjectContext();
|
||||||
const autoRedirect = useAutoRedirect();
|
const autoRedirect = useAutoRedirect();
|
||||||
const [search] = useSearchParams();
|
const [search] = useSearchParams();
|
||||||
const [identity, setIdentity] = useState<string>("");
|
const [identity, setIdentity] = useState<string>("");
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>("");
|
||||||
const projectId = search.get("projectId") ?? project;
|
|
||||||
|
|
||||||
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
|
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
|
||||||
|
|
||||||
|
@ -52,6 +51,7 @@ export default function AuthenticateView() {
|
||||||
function detectEnter(e: KeyboardEvent) {
|
function detectEnter(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter") submitCredentials();
|
if (e.key === "Enter") submitCredentials();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth) return <Navigate to={rootLink(Links.AutoRedirect)} />;
|
if (auth) return <Navigate to={rootLink(Links.AutoRedirect)} />;
|
||||||
return (
|
return (
|
||||||
<Box width="100%" height="90vh" display="flex" alignItems="center" justifyContent="center">
|
<Box width="100%" height="90vh" display="flex" alignItems="center" justifyContent="center">
|
||||||
|
|
|
@ -1,11 +1,61 @@
|
||||||
import { Flex, Text, Center } from "@chakra-ui/react";
|
import { Flex, Text, Center, Box, Skeleton, Stack } from "@chakra-ui/react";
|
||||||
|
import ProjectTile, { ProjectTileSkeleton } from "@src/components/projects/ProjectTile";
|
||||||
|
import { useProjectContext } from "@src/ctx/ProjectContext";
|
||||||
|
import { useProjectList } from "@src/util/api/queries";
|
||||||
|
import { GuardedContent } from "@src/util/guards";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
const skeletonCount = 12;
|
||||||
|
const loadingFallbacks: ReactNode[] = new Array(skeletonCount)
|
||||||
|
.fill(0)
|
||||||
|
.map((_v: number, i: number) => <ProjectTileSkeleton key={i} />);
|
||||||
|
|
||||||
export default function ProjectView() {
|
export default function ProjectView() {
|
||||||
|
const { projectId } = useProjectContext();
|
||||||
|
const { data, isLoading } = useProjectList(projectId);
|
||||||
|
const projects = (data?.childProjects ?? []).filter((p) => p.id !== data?.project.id);
|
||||||
return (
|
return (
|
||||||
<Flex h="100vh" w="100%">
|
<Box h="calc(100vh - 1.5rem)" w="100%" overflowY={isLoading ? "hidden" : "unset"}>
|
||||||
|
<Flex wrap="wrap" m="1.5rem" gap="2rem">
|
||||||
|
<Flex wrap="wrap" w="100%">
|
||||||
|
<Text fontSize="18px" h="2rem" w="100%" fontWeight={800}>
|
||||||
|
Project
|
||||||
|
</Text>
|
||||||
<Center w="100%">
|
<Center w="100%">
|
||||||
<Text>Project Management</Text>
|
{isLoading && <ProjectTileSkeleton />}
|
||||||
|
{!!data?.project && <ProjectTile project={data?.project} />}
|
||||||
</Center>
|
</Center>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
<GuardedContent guard={projects.length > 0}>
|
||||||
|
<Flex wrap="wrap" w="100%">
|
||||||
|
<Text fontSize="18px" h="2rem" w="100%" fontWeight={800}>
|
||||||
|
Child Projects
|
||||||
|
</Text>
|
||||||
|
<Center gap="2rem" flexWrap="wrap" w="100%">
|
||||||
|
{isLoading && loadingFallbacks}
|
||||||
|
{projects.map((p) => (
|
||||||
|
<ProjectTile key={p.id} project={p} />
|
||||||
|
))}
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</GuardedContent>
|
||||||
|
|
||||||
|
<GuardedContent guard={projects.length === 0}>
|
||||||
|
<Flex h="40vh" w="100%">
|
||||||
|
<Center w="100%" flexWrap="wrap">
|
||||||
|
<Box bg="background.paper" p="1.75rem" borderRadius=".5rem">
|
||||||
|
<Text w="100%" textAlign="center">
|
||||||
|
No child projects found!
|
||||||
|
</Text>
|
||||||
|
<Text w="100%" textAlign="center">
|
||||||
|
Create one by clicking here
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</GuardedContent>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue