First commit
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
Refine,
|
||||
Authenticated,
|
||||
} from "@refinedev/core";
|
||||
import {
|
||||
useNotificationProvider,
|
||||
ThemedLayout,
|
||||
ErrorComponent,
|
||||
RefineThemes,
|
||||
} from "@refinedev/antd";
|
||||
import {
|
||||
DashboardOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
import routerProvider, {
|
||||
NavigateToResource,
|
||||
CatchAllNavigate,
|
||||
UnsavedChangesNotifier,
|
||||
DocumentTitleHandler,
|
||||
} from "@refinedev/react-router";
|
||||
import { BrowserRouter, Routes, Route, Outlet, Navigate } from "react-router";
|
||||
import { App as AntdApp, ConfigProvider } from "antd";
|
||||
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import "@refinedev/antd/dist/reset.css";
|
||||
|
||||
import { authProvider, dataProvider, liveProvider } from "@ng-org/refine-providers";
|
||||
|
||||
import dataModels from "./shapes/data-models";
|
||||
import AuthPage from "./components/AuthPage";
|
||||
import { PostList, PostCreate, PostEdit, PostShow } from "../src/pages/posts";
|
||||
import { DashboardPage } from "../src/pages/dashboard";
|
||||
|
||||
import { getSession } from "./utils/initNg";
|
||||
|
||||
const App: React.FC = () => (
|
||||
<BrowserRouter basename={import.meta.env.MODE === 'production' ? '/nextgraph-refine-app/' : '/'}>
|
||||
<ConfigProvider theme={RefineThemes.Blue}>
|
||||
<AntdApp>
|
||||
<Refine
|
||||
// authProvider={authProvider}
|
||||
dataProvider={dataProvider({ dataModels, getSession })}
|
||||
liveProvider={liveProvider({ dataModels })}
|
||||
routerProvider={routerProvider}
|
||||
resources={[
|
||||
// {
|
||||
// name: "dashboard",
|
||||
// list: "/",
|
||||
// meta: {
|
||||
// label: "Dashboard",
|
||||
// icon: <DashboardOutlined />,
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: "posts",
|
||||
list: "/posts",
|
||||
create: "/posts/create",
|
||||
show: "/posts/show/:id",
|
||||
edit: "/posts/edit/:id",
|
||||
meta: {
|
||||
canDelete: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
notificationProvider={useNotificationProvider}
|
||||
options={{
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
liveMode: "auto",
|
||||
disableTelemetry: true,
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
// <Authenticated
|
||||
// key="authenticated-routes"
|
||||
// fallback={<CatchAllNavigate to="/login" />}
|
||||
// >
|
||||
<ThemedLayout>
|
||||
<Outlet />
|
||||
</ThemedLayout>
|
||||
// </Authenticated>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="/" />} />
|
||||
{/* <Route index element={<DashboardPage />} /> */}
|
||||
|
||||
<Route path="/posts">
|
||||
<Route index element={<PostList />} />
|
||||
<Route path="create" element={<PostCreate />} />
|
||||
<Route path="edit/:id" element={<PostEdit />} />
|
||||
<Route path="show/:id" element={<PostShow />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* <Route
|
||||
element={
|
||||
<Authenticated key="auth-pages" fallback={<Outlet />}>
|
||||
<NavigateToResource resource="posts" />
|
||||
</Authenticated>
|
||||
}
|
||||
>
|
||||
<Route path="/login" element={<AuthPage />} />
|
||||
<Route path="/register" element={<AuthPage />} />
|
||||
</Route> */}
|
||||
|
||||
<Route
|
||||
element={
|
||||
// <Authenticated key="catch-all">
|
||||
<ThemedLayout>
|
||||
<Outlet />
|
||||
</ThemedLayout>
|
||||
// </Authenticated>
|
||||
}
|
||||
>
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<UnsavedChangesNotifier />
|
||||
<DocumentTitleHandler />
|
||||
</Refine>
|
||||
</AntdApp>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,172 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
import {
|
||||
type LoginPageProps,
|
||||
type LoginFormTypes,
|
||||
} from "@refinedev/core";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Layout,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
type CardProps,
|
||||
type LayoutProps,
|
||||
type FormProps,
|
||||
theme,
|
||||
} from "antd";
|
||||
import { useLogin, useTranslate } from "@refinedev/core";
|
||||
import { ThemedTitle } from "@refinedev/antd";
|
||||
|
||||
export const layoutStyles: CSSProperties = {};
|
||||
|
||||
export const containerStyles: CSSProperties = {
|
||||
maxWidth: "400px",
|
||||
margin: "auto",
|
||||
padding: "32px",
|
||||
boxShadow:
|
||||
"0px 2px 4px rgba(0, 0, 0, 0.02), 0px 1px 6px -1px rgba(0, 0, 0, 0.02), 0px 1px 2px rgba(0, 0, 0, 0.03)",
|
||||
};
|
||||
|
||||
export const headStyles: CSSProperties = {
|
||||
borderBottom: 0,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
export const bodyStyles: CSSProperties = { padding: 0 };
|
||||
|
||||
export const titleStyles: CSSProperties = {
|
||||
textAlign: "center",
|
||||
marginBottom: 0,
|
||||
fontSize: "24px",
|
||||
lineHeight: "32px",
|
||||
fontWeight: 700,
|
||||
overflowWrap: "break-word",
|
||||
hyphens: "manual",
|
||||
textOverflow: "unset",
|
||||
whiteSpace: "pre-wrap",
|
||||
};
|
||||
|
||||
type LoginProps = LoginPageProps<LayoutProps, CardProps, FormProps>;
|
||||
|
||||
export const AuthPage: React.FC<LoginProps> = ({
|
||||
contentProps,
|
||||
wrapperProps,
|
||||
renderContent,
|
||||
formProps,
|
||||
title,
|
||||
hideForm,
|
||||
mutationVariables,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [form] = Form.useForm<LoginFormTypes>();
|
||||
const translate = useTranslate();
|
||||
|
||||
const { mutate: login, isPending } = useLogin<LoginFormTypes>();
|
||||
|
||||
const PageTitle =
|
||||
title === false ? null : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginBottom: "32px",
|
||||
fontSize: "20px",
|
||||
}}
|
||||
>
|
||||
{title ?? <ThemedTitle collapsed={false} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
const CardContent = (
|
||||
<Card
|
||||
// title={CardTitle}
|
||||
styles={{
|
||||
header: headStyles,
|
||||
body: bodyStyles,
|
||||
}}
|
||||
style={{
|
||||
...containerStyles,
|
||||
backgroundColor: token.colorBgElevated,
|
||||
}}
|
||||
{...(contentProps ?? {})}
|
||||
>
|
||||
<Form<LoginFormTypes>
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={(values) => login({ ...values, ...mutationVariables })}
|
||||
requiredMark={false}
|
||||
initialValues={{
|
||||
brokerUrl: 'https://nextgraph.eu'
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
name="brokerUrl"
|
||||
label={translate("pages.login.fields.brokerUrl", "Broker URL")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: translate(
|
||||
"pages.login.errors.requiredBrokerUrl",
|
||||
"Broker URL is required",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "url",
|
||||
message: translate(
|
||||
"pages.login.errors.validBrokerUrl",
|
||||
"Invalid broker URL",
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={translate("pages.login.fields.brokerUrl", "Broker URL")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
loading={isPending}
|
||||
block
|
||||
>
|
||||
{translate("pages.login.signin", "Sign in")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout style={layoutStyles} {...(wrapperProps ?? {})}>
|
||||
<Row
|
||||
justify="center"
|
||||
align={hideForm ? "top" : "middle"}
|
||||
style={{
|
||||
padding: "16px 0",
|
||||
minHeight: "100dvh",
|
||||
paddingTop: hideForm ? "15dvh" : "16px",
|
||||
}}
|
||||
>
|
||||
<Col xs={22}>
|
||||
{renderContent ? (
|
||||
renderContent(CardContent, PageTitle)
|
||||
) : (
|
||||
<>
|
||||
{PageTitle}
|
||||
{CardContent}
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthPage;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const container = document.getElementById("root");
|
||||
// eslint-disable-next-line
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useGetIdentity, usePermissions } from "@refinedev/core";
|
||||
import { Row, Col, Card, Avatar, Typography, Space } from "antd";
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DashboardPage: React.FC = () => {
|
||||
const { data: identity } = useGetIdentity<{
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
}>();
|
||||
const permissions = usePermissions<string[], { permissions: string[] }>({
|
||||
params: { permissions: ["admin"] },
|
||||
});
|
||||
|
||||
return (
|
||||
<Row gutter={20}>
|
||||
<Col span={6}>
|
||||
<Card
|
||||
title="Identity"
|
||||
style={{ height: "300px", borderRadius: "15px" }}
|
||||
headStyle={{ textAlign: "center" }}
|
||||
>
|
||||
<Space align="center" direction="horizontal">
|
||||
<Avatar size="large" src={identity?.avatar} />
|
||||
<Text>{identity?.name}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card
|
||||
title="Permissions"
|
||||
style={{ height: "300px", borderRadius: "15px" }}
|
||||
headStyle={{ textAlign: "center" }}
|
||||
>
|
||||
<Text>{permissions.data}</Text>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Create, useForm } from "@refinedev/antd";
|
||||
import { Form, Input } from "antd";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import type { Note } from "../../shapes/orm/post.typings";
|
||||
|
||||
export const PostCreate = () => {
|
||||
const { formProps, saveButtonProps } = useForm<Note>();
|
||||
|
||||
return (
|
||||
<Create saveButtonProps={saveButtonProps}>
|
||||
<Form {...formProps} layout="vertical">
|
||||
<Form.Item
|
||||
label="Title"
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="Enter a post title" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Content"
|
||||
name="content"
|
||||
>
|
||||
<MDEditor data-color-mode="light" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Create>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Edit, useForm, RefreshButton } from "@refinedev/antd";
|
||||
import { useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
|
||||
import { Alert, Form, Input } from "antd";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import type { Note } from "../../shapes/orm/post.typings";
|
||||
|
||||
export const PostEdit = () => {
|
||||
const [deprecated, setDeprecated] = useState<boolean>(false);
|
||||
|
||||
const { list } = useNavigation();
|
||||
const { open } = useNotification();
|
||||
|
||||
const onLiveEvent = useCallback((event: LiveEvent) => {
|
||||
console.log('onLiveEvent', event);
|
||||
if (event.type === "deleted") {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "This post has been deleted",
|
||||
});
|
||||
list("posts");
|
||||
} else if (event.type === "updated") {
|
||||
setDeprecated(true);
|
||||
}
|
||||
}, [open, list]);
|
||||
|
||||
const { formProps, saveButtonProps, query: queryResult } = useForm<Note>({
|
||||
liveMode: 'manual',
|
||||
onLiveEvent,
|
||||
});
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryResult?.refetch();
|
||||
setDeprecated(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Edit saveButtonProps={saveButtonProps}>
|
||||
<Form {...formProps} layout="vertical">
|
||||
{deprecated && (
|
||||
<Alert
|
||||
message="This post has been updated. Refresh to see changes."
|
||||
type="warning"
|
||||
style={{ marginBottom: 20 }}
|
||||
action={<RefreshButton size="small" onClick={handleRefresh} />}
|
||||
/>
|
||||
)}
|
||||
<Form.Item
|
||||
label="Title"
|
||||
name="title"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Content"
|
||||
name="content"
|
||||
>
|
||||
<MDEditor data-color-mode="light" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Edit>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./list";
|
||||
export * from "./create";
|
||||
export * from "./edit";
|
||||
export * from "./show";
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
List,
|
||||
useTable,
|
||||
EditButton,
|
||||
ShowButton,
|
||||
DeleteButton,
|
||||
CreateButton,
|
||||
} from "@refinedev/antd";
|
||||
import { useNavigation } from "@refinedev/core";
|
||||
import { Table, Space } from "antd";
|
||||
import type { Note } from "../../shapes/orm/post.typings";
|
||||
|
||||
const shortenNuri = (nuri: string) => {
|
||||
return nuri?.split(':')[3]?.slice(0, 7);
|
||||
};
|
||||
|
||||
export const PostList = () => {
|
||||
const { tableProps } = useTable<Note>();
|
||||
const { show } = useNavigation();
|
||||
|
||||
return (
|
||||
<List headerButtons={<CreateButton />}>
|
||||
<Table
|
||||
{...tableProps}
|
||||
rowKey="@graph"
|
||||
onRow={(record) => ({
|
||||
onClick: () => show("posts", record["@graph"]),
|
||||
style: { cursor: "pointer" },
|
||||
})}
|
||||
>
|
||||
<Table.Column dataIndex="@graph" title="ID" render={shortenNuri} />
|
||||
<Table.Column dataIndex="title" title="Title" />
|
||||
<Table.Column dataIndex="content" title="Content" />
|
||||
<Table.Column<Note>
|
||||
title="Actions"
|
||||
dataIndex="actions"
|
||||
align="right"
|
||||
render={(_, record) => (
|
||||
<Space onClick={(e) => e.stopPropagation()}>
|
||||
<EditButton hideText size="small" recordItemId={record['@graph']} />
|
||||
<ShowButton hideText size="small" recordItemId={record['@graph']} />
|
||||
<DeleteButton hideText size="small" recordItemId={record['@graph']} />
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
</Table>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from "react";
|
||||
import { useShow, useNavigation, useNotification, type LiveEvent } from "@refinedev/core";
|
||||
import { Show, MarkdownField } from "@refinedev/antd";
|
||||
import { Typography } from "antd";
|
||||
import type { Note } from "../../shapes/orm/post.typings";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export const PostShow = () => {
|
||||
const { list } = useNavigation();
|
||||
const { open } = useNotification();
|
||||
|
||||
const onLiveEvent = useCallback((event: LiveEvent) => {
|
||||
if (event.type === "deleted") {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: "This post has been deleted",
|
||||
});
|
||||
list("posts");
|
||||
}
|
||||
}, [open, list]);
|
||||
|
||||
const { query: queryResult } = useShow<Note>({ liveMode: "auto", onLiveEvent });
|
||||
const { data, isLoading } = queryResult;
|
||||
const record = data?.data;
|
||||
|
||||
return (
|
||||
<Show isLoading={isLoading}>
|
||||
<Title level={5}>Id</Title>
|
||||
<Text>{record?.['@graph']}</Text>
|
||||
<Title level={5}>Title</Title>
|
||||
<Text>{record?.title}</Text>
|
||||
<Title level={5}>Content</Title>
|
||||
<MarkdownField value={record?.content} />
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DataModels } from "@ng-org/refine-providers";
|
||||
import { NoteShapeType } from "./orm/post.shapeTypes";
|
||||
|
||||
const dataModels : DataModels = {
|
||||
posts: {
|
||||
shapeType: NoteShapeType
|
||||
}
|
||||
}
|
||||
|
||||
export default dataModels;
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Schema } from "@ng-org/shex-orm";
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* postSchema: Schema for post
|
||||
* =============================================================================
|
||||
*/
|
||||
export const postSchema = {
|
||||
"https://www.w3.org/ns/activitystreams#Note": {
|
||||
iri: "https://www.w3.org/ns/activitystreams#Note",
|
||||
predicates: [
|
||||
{
|
||||
dataTypes: [
|
||||
{
|
||||
valType: "iri",
|
||||
literals: ["https://www.w3.org/ns/activitystreams#Note"],
|
||||
},
|
||||
],
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "@type",
|
||||
},
|
||||
{
|
||||
dataTypes: [
|
||||
{
|
||||
valType: "string",
|
||||
},
|
||||
],
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
iri: "https://www.w3.org/ns/activitystreams#title",
|
||||
readablePredicate: "title",
|
||||
},
|
||||
{
|
||||
dataTypes: [
|
||||
{
|
||||
valType: "string",
|
||||
},
|
||||
],
|
||||
maxCardinality: 1,
|
||||
minCardinality: 0,
|
||||
iri: "https://www.w3.org/ns/activitystreams#content",
|
||||
readablePredicate: "content",
|
||||
},
|
||||
],
|
||||
},
|
||||
} as const satisfies Schema;
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ShapeType } from "@ng-org/shex-orm";
|
||||
import { postSchema } from "./post.schema";
|
||||
import type { Note } from "./post.typings";
|
||||
|
||||
// ShapeTypes for post
|
||||
export const NoteShapeType = {
|
||||
schema: postSchema,
|
||||
shape: "https://www.w3.org/ns/activitystreams#Note",
|
||||
} as const satisfies ShapeType<Note>;
|
||||
@@ -0,0 +1,37 @@
|
||||
export type IRI = string;
|
||||
|
||||
/**
|
||||
* =============================================================================
|
||||
* Typescript Typings for post
|
||||
* =============================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* Note Type
|
||||
*/
|
||||
export interface Note {
|
||||
/**
|
||||
* The graph NURI.
|
||||
*/
|
||||
readonly "@graph": IRI;
|
||||
/**
|
||||
* The subject IRI.
|
||||
*/
|
||||
readonly "@id": IRI;
|
||||
/**
|
||||
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
|
||||
*/
|
||||
"@type": "https://www.w3.org/ns/activitystreams#Note";
|
||||
/**
|
||||
* The title of the post
|
||||
*
|
||||
* Original IRI: https://www.w3.org/ns/activitystreams#title
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The content of the post
|
||||
*
|
||||
* Original IRI: https://www.w3.org/ns/activitystreams#content
|
||||
*/
|
||||
content?: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
PREFIX as: <https://www.w3.org/ns/activitystreams#>
|
||||
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
||||
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
|
||||
|
||||
as:Note {
|
||||
a [as:Note] ;
|
||||
as:title xsd:string
|
||||
// rdfs:comment "The title of the post" ;
|
||||
as:content xsd:string ?
|
||||
// rdfs:comment "The content of the post" ;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ng, init, Session } from "@ng-org/web";
|
||||
import { initNg } from "@ng-org/orm";
|
||||
|
||||
export let session: Session | undefined;
|
||||
|
||||
init(
|
||||
async (event: any) => {
|
||||
session = event.session;
|
||||
// The ORM needs to have access to ng,
|
||||
// the interface to the engine running in WASM.
|
||||
initNg(ng, session!);
|
||||
},
|
||||
true,
|
||||
[],
|
||||
);
|
||||
|
||||
export const getSession = () => session;
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user