Skip to content

Commit

Permalink
reorganize, make projects listing and creation work
Browse files Browse the repository at this point in the history
  • Loading branch information
Mazuh committed Nov 26, 2023
1 parent 4b7cefb commit 6cde11d
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 56 deletions.
10 changes: 5 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import "@testing-library/jest-dom";
import { act, render, screen } from "@testing-library/react";
import App from "./App";
import {
makeOpfsAdapterSingleton,
makeOpfsFileAdapterSingleton,
isPersistenceSupported,
} from "./services/origin-private-file-system";

jest.mock("./services/origin-private-file-system.ts", () => ({
makeOpfsAdapterSingleton: jest
makeOpfsFileAdapterSingleton: jest
.fn()
.mockReturnValue(
jest.fn().mockResolvedValue({ retrieve: jest.fn(), persist: jest.fn() })
Expand All @@ -19,8 +19,8 @@ describe("App", () => {
beforeEach(() => {
jest.useFakeTimers();

(makeOpfsAdapterSingleton as jest.Mock).mockClear();
(makeOpfsAdapterSingleton as jest.Mock).mockReturnValue(async () => ({
(makeOpfsFileAdapterSingleton as jest.Mock).mockClear();
(makeOpfsFileAdapterSingleton as jest.Mock).mockReturnValue(async () => ({
retrieve: jest
.fn()
.mockResolvedValue("d48c2b8f-e557-4503-8fac-4561bba582ac"),
Expand All @@ -39,7 +39,7 @@ describe("App", () => {

await act(() => jest.runAllTimers());

(makeOpfsAdapterSingleton as jest.Mock).mockResolvedValue({
(makeOpfsFileAdapterSingleton as jest.Mock).mockResolvedValue({
retrieve: jest
.fn()
.mockResolvedValue("01fb9ee8-926c-483e-9eb5-f020762d4b00"),
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "./App.css";
import { useEffect } from "react";
import useClientSession from "./hooks/useClientSession";
import { ProjectsManagementPage } from "./features/projects-management/ProjectsManagementPage";

export default function App() {
const { isActive, doActiveThisSession, isOfflineModeSupported } =
Expand Down Expand Up @@ -37,6 +38,7 @@ export default function App() {
<div>
<h1>Postmaiden</h1>
<em>Snake? Do you think love can bloom even on the battlefield?</em>
<ProjectsManagementPage />
</div>
);
}
8 changes: 4 additions & 4 deletions src/entities/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ export interface Project {
uuid: string;
name: string;
sections: ProjectSection[];
requests: RequestEntry[];
requests: ProjectRequest[];
}

export interface ProjectSection {
uuid: string;
name: string;
}

export interface RequestEntry {
export interface ProjectRequest {
uuid: string;
url: string;
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
headers: HeaderEntry[];
headers: ProjectRequestHeader[];
body: string;
}

export interface HeaderEntry {
export interface ProjectRequestHeader {
key: string;
value: string;
isEnabled: boolean;
Expand Down
21 changes: 21 additions & 0 deletions src/features/projects-management/ProjectsManagementContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, useContext } from "react";
import { ProjectListingItem } from "./projects-management-entities";

export interface ProjectsManagementContextValue {
items: ProjectListingItem[];
create: (name: string) => Promise<void>;
isLoading: boolean;
isError: Error | null;
}

export const ProjectsManagementContext =
createContext<ProjectsManagementContextValue | null>(null);

export function useProjectsManagement(): ProjectsManagementContextValue {
const contextValue = useContext(ProjectsManagementContext);
if (contextValue === null) {
throw new Error("Hook used outside its context.");
}

return contextValue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ReactNode, useCallback, useEffect, useState } from "react";
import {
persistNewProjectListingItem,
retrieveProjectsListing,
} from "./opfs-projects-listing-service";
import { ProjectListingItem } from "./projects-management-entities";
import { ProjectsManagementContext } from "./ProjectsManagementContext";

export function ProjectsManagementContextProvider({
children,
}: {
children: ReactNode;
}) {
const [items, setProjects] = useState<ProjectListingItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isError, setError] = useState<Error | null>(null);

const pullProjectsListing = useCallback(async () => {
setError(null);
setIsLoading(true);
retrieveProjectsListing()
.then((projects) => setProjects(projects.items))
.catch((error) => setError(error))
.finally(() => setIsLoading(false));
}, []);

useEffect(() => {
pullProjectsListing();
}, [pullProjectsListing]);

const create = useCallback(
async (name: string) => {
setError(null);
setIsLoading(true);
persistNewProjectListingItem(name)
.catch((error) => setError(error))
.finally(() => setIsLoading(false))
.then(() => pullProjectsListing());
},
[pullProjectsListing]
);

return (
<ProjectsManagementContext.Provider
value={{ items, create, isLoading, isError }}
>
{children}
</ProjectsManagementContext.Provider>
);
}
65 changes: 65 additions & 0 deletions src/features/projects-management/ProjectsManagementPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { FormEvent, useCallback, useState } from "react";
import { useProjectsManagement } from "./ProjectsManagementContext";
import { ProjectsManagementContextProvider } from "./ProjectsManagementContextProvider";

export function ProjectsManagementPage() {
return (
<ProjectsManagementContextProvider>
<main>
<ProjectForm />
<ProjectsList />
</main>
</ProjectsManagementContextProvider>
);
}

function ProjectsList() {
const { items: projects, isLoading, isError } = useProjectsManagement();

if (isLoading) {
return <div>Loading...</div>;
}

if (isError) {
return <div>Error: {isError.message}</div>;
}

return (
<ul>
{projects.map((project) => (
<li key={project.uuid}>{project.name}</li>
))}
</ul>
);
}

function ProjectForm() {
const { create } = useProjectsManagement();

const [name, setName] = useState("");

const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
create(name);
},
[create, name]
);

return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Project name</label>
<input
type="text"
id="name"
value={name}
onChange={(event) => setName(event.target.value)}
autoComplete="off"
maxLength={50}
spellCheck={false}
required
/>
<button type="submit">Create</button>
</form>
);
}
62 changes: 62 additions & 0 deletions src/features/projects-management/opfs-projects-listing-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { v4 as uuidv4 } from "uuid";
import { Project } from "../../entities/management";
import {
makeOpfsFileAdapter,
makeOpfsMainDirAdapter,
} from "../../services/origin-private-file-system";
import {
ProjectListing,
ProjectListingItem,
} from "./projects-management-entities";

export async function retrieveProjectsListing(): Promise<ProjectListing> {
const opfsDir = await makeOpfsMainDirAdapter({
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});
const filenames = await opfsDir.retrieveFilenames();
return {
items: filenames.map((filename) => {
const rFilename =
/(?<uuid>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})_(?<name>[A-Za-z ]+)\.json/;
const match = filename.match(rFilename);
if (!match || !match.groups) {
throw new Error(
`Invalid project filename (corrupted data?): ${filename}`
);
}

return {
uuid: match.groups.uuid,
name: match.groups.name,
};
}),
};
}

export async function persistNewProjectListingItem(
name: string
): Promise<ProjectListingItem> {
if (!name) {
throw new Error("Project name cannot be empty.");
}

const item: ProjectListingItem = {
uuid: uuidv4(),
name: name.trim().replace(".json", ""),
};

const file = await makeOpfsFileAdapter<Project>({
filename: `${item.uuid}_${item.name}.json`,
subdir: PROJECTS_OPFS_SUBDIRECTORY,
});
await file.persist({
uuid: item.uuid,
name: item.name,
sections: [],
requests: [],
});

return item;
}

export const PROJECTS_OPFS_SUBDIRECTORY = "projects";
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ProjectListing = {
items: ProjectListingItem[];
};

export interface ProjectListingItem {
uuid: string;
name: string;
}
6 changes: 4 additions & 2 deletions src/services/opfs-client-session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { validate as validateUuid } from "uuid";
import { makeOpfsAdapterSingleton } from "./origin-private-file-system";
import { makeOpfsFileAdapterSingleton } from "./origin-private-file-system";

const getOpfsAdapter = makeOpfsAdapterSingleton<string>("client-session.txt");
const getOpfsAdapter = makeOpfsFileAdapterSingleton<string>({
filename: "client-session.txt",
});

export async function retrieveClientSessionUuid(): Promise<string> {
const opfs = await getOpfsAdapter();
Expand Down
Loading

0 comments on commit 6cde11d

Please sign in to comment.