diff --git a/apps/console/src/App.tsx b/apps/console/src/App.tsx
index eafe1f673..0e74bd728 100644
--- a/apps/console/src/App.tsx
+++ b/apps/console/src/App.tsx
@@ -6,6 +6,7 @@ import { HelmetProvider } from "react-helmet-async";
import { RelayEnvironmentProvider } from "react-relay";
import { BrowserRouter, Route, Routes, useLocation } from "react-router";
import "./App.css";
+import "./styles/policy-content.css";
import ErrorBoundary from "./components/ErrorBoundary";
import ConsoleLayout from "./layouts/ConsoleLayout";
import AuthLayout from "./layouts/AuthLayout";
diff --git a/apps/console/src/components/PolicyEditor.css b/apps/console/src/components/PolicyEditor.css
new file mode 100644
index 000000000..14d5d4c17
--- /dev/null
+++ b/apps/console/src/components/PolicyEditor.css
@@ -0,0 +1,230 @@
+.policy-editor-container {
+ border-radius: 0.5rem;
+ position: relative;
+ line-height: 1.5;
+ font-weight: 400;
+ margin-top: 0.5rem;
+ border: 1px solid hsl(var(--border));
+ display: block;
+ width: 100%;
+}
+
+.policy-editor {
+ background: #fff;
+ position: relative;
+ border-radius: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.policy-editor-inner {
+ background: #fff;
+ position: relative;
+ border-radius: 0 0 0.5rem 0.5rem;
+ flex: 1;
+ min-height: 250px;
+}
+
+.policy-editor-input {
+ min-height: 250px;
+ max-height: 500px;
+ resize: none;
+ font-size: 15px;
+ position: relative;
+ tab-size: 1;
+ outline: 0;
+ padding: 15px 10px;
+ overflow-y: auto;
+ width: 100%;
+ display: block;
+ color: #000;
+}
+
+.policy-editor-placeholder {
+ color: #999;
+ overflow: hidden;
+ position: absolute;
+ text-overflow: ellipsis;
+ top: 15px;
+ left: 10px;
+ font-size: 15px;
+ user-select: none;
+ display: inline-block;
+ pointer-events: none;
+}
+
+.policy-editor-paragraph {
+ margin: 0 0 15px 0;
+ position: relative;
+}
+
+.policy-editor .toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 8px;
+ border-bottom: 1px solid hsl(var(--border));
+ background-color: hsl(var(--muted));
+ border-radius: 0.5rem 0.5rem 0 0;
+}
+
+.policy-editor .toolbar button {
+ border: 0;
+ display: flex;
+ background: none;
+ border-radius: 4px;
+ padding: 8px;
+ cursor: pointer;
+ vertical-align: middle;
+ align-items: center;
+ justify-content: center;
+}
+
+.policy-editor .toolbar button:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.policy-editor .toolbar button.active {
+ background-color: rgba(0, 0, 0, 0.1);
+}
+
+.policy-editor .toolbar .divider {
+ width: 1px;
+ background-color: hsl(var(--border));
+ margin: 0 8px;
+ height: 24px;
+}
+
+/* Typography */
+.editor-heading-h1 {
+ font-size: 24px;
+ font-weight: 700;
+ margin: 0 0 12px 0;
+}
+
+.editor-heading-h2 {
+ font-size: 20px;
+ font-weight: 700;
+ margin: 10px 0;
+}
+
+.editor-heading-h3 {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 8px 0;
+}
+
+.editor-heading-h4 {
+ font-size: 16px;
+ font-weight: 600;
+ margin: 8px 0;
+}
+
+.editor-heading-h5 {
+ font-size: 14px;
+ font-weight: 600;
+ margin: 8px 0;
+}
+
+.editor-quote {
+ margin: 0;
+ margin-left: 20px;
+ padding-left: 16px;
+ border-left: 4px solid hsl(var(--border));
+ color: hsl(var(--muted-foreground));
+}
+
+.editor-list-ol {
+ padding: 0;
+ margin: 0 0 0 16px;
+}
+
+.editor-list-ul {
+ padding: 0;
+ margin: 0 0 0 16px;
+}
+
+.editor-listitem {
+ margin: 8px 0 0 0;
+}
+
+.editor-nested-listitem {
+ list-style-type: none;
+}
+
+/* Text formatting */
+.editor-text-bold {
+ font-weight: bold;
+}
+
+.editor-text-italic {
+ font-style: italic;
+}
+
+.editor-text-underline {
+ text-decoration: underline;
+}
+
+.editor-text-strikethrough {
+ text-decoration: line-through;
+}
+
+.editor-text-underlineStrikethrough {
+ text-decoration: underline line-through;
+}
+
+.editor-text-code {
+ background-color: hsl(var(--muted));
+ padding: 1px 4px;
+ border-radius: 4px;
+ font-family: monospace;
+}
+
+.editor-link {
+ color: hsl(var(--primary));
+ text-decoration: underline;
+}
+
+.editor-code {
+ background-color: hsl(var(--muted));
+ font-family: monospace;
+ display: block;
+ padding: 8px 16px;
+ line-height: 1.6;
+ font-size: 13px;
+ border-radius: 0.5rem;
+ margin: 8px 0;
+}
+
+/* Code highlight */
+.editor-tokenComment {
+ color: slategray;
+}
+
+.editor-tokenPunctuation {
+ color: #999;
+}
+
+.editor-tokenProperty {
+ color: #905;
+}
+
+.editor-tokenSelector {
+ color: #690;
+}
+
+.editor-tokenOperator {
+ color: #9a6e3a;
+}
+
+.editor-tokenAttr {
+ color: #07a;
+}
+
+.editor-tokenVariable {
+ color: #e90;
+}
+
+.editor-tokenFunction {
+ color: #dd4a68;
+}
\ No newline at end of file
diff --git a/apps/console/src/components/PolicyEditor.tsx b/apps/console/src/components/PolicyEditor.tsx
new file mode 100644
index 000000000..958d767d8
--- /dev/null
+++ b/apps/console/src/components/PolicyEditor.tsx
@@ -0,0 +1,381 @@
+import { useEffect, useState } from "react";
+import { LexicalComposer } from "@lexical/react/LexicalComposer";
+import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
+import { ContentEditable } from "@lexical/react/LexicalContentEditable";
+import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
+import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
+import { HeadingNode, QuoteNode } from "@lexical/rich-text";
+import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
+import { ListItemNode, ListNode } from "@lexical/list";
+import { CodeHighlightNode, CodeNode } from "@lexical/code";
+import { AutoLinkNode, LinkNode } from "@lexical/link";
+import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
+import { ListPlugin } from "@lexical/react/LexicalListPlugin";
+import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
+import { TRANSFORMERS } from "@lexical/markdown";
+import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html";
+import { $getRoot, $insertNodes, EditorState, LexicalEditor } from "lexical";
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
+import {
+ Bold,
+ Italic,
+ Underline,
+ List,
+ ListOrdered,
+ Heading1,
+ Heading2,
+} from "lucide-react";
+import { $isRangeSelection, FORMAT_TEXT_COMMAND, $getSelection } from "lexical";
+import { $createHeadingNode, $isHeadingNode } from "@lexical/rich-text";
+import { $setBlocksType } from "@lexical/selection";
+import { $createParagraphNode } from "lexical";
+import {
+ INSERT_ORDERED_LIST_COMMAND,
+ INSERT_UNORDERED_LIST_COMMAND,
+} from "@lexical/list";
+
+import "./PolicyEditor.css";
+
+// Theme for the editor
+const theme = {
+ ltr: "ltr",
+ rtl: "rtl",
+ paragraph: "editor-paragraph",
+ quote: "editor-quote",
+ heading: {
+ h1: "editor-heading-h1",
+ h2: "editor-heading-h2",
+ h3: "editor-heading-h3",
+ h4: "editor-heading-h4",
+ h5: "editor-heading-h5",
+ },
+ list: {
+ nested: {
+ listitem: "editor-nested-listitem",
+ },
+ ol: "editor-list-ol",
+ ul: "editor-list-ul",
+ listitem: "editor-listitem",
+ },
+ image: "editor-image",
+ link: "editor-link",
+ text: {
+ bold: "editor-text-bold",
+ italic: "editor-text-italic",
+ underline: "editor-text-underline",
+ strikethrough: "editor-text-strikethrough",
+ underlineStrikethrough: "editor-text-underlineStrikethrough",
+ code: "editor-text-code",
+ },
+ code: "editor-code",
+ codeHighlight: {
+ atrule: "editor-tokenAttr",
+ attr: "editor-tokenAttr",
+ boolean: "editor-tokenProperty",
+ builtin: "editor-tokenSelector",
+ cdata: "editor-tokenComment",
+ char: "editor-tokenSelector",
+ class: "editor-tokenFunction",
+ "class-name": "editor-tokenFunction",
+ comment: "editor-tokenComment",
+ constant: "editor-tokenProperty",
+ deleted: "editor-tokenProperty",
+ doctype: "editor-tokenComment",
+ entity: "editor-tokenOperator",
+ function: "editor-tokenFunction",
+ important: "editor-tokenVariable",
+ inserted: "editor-tokenSelector",
+ keyword: "editor-tokenAttr",
+ namespace: "editor-tokenVariable",
+ number: "editor-tokenProperty",
+ operator: "editor-tokenOperator",
+ prolog: "editor-tokenComment",
+ property: "editor-tokenProperty",
+ punctuation: "editor-tokenPunctuation",
+ regex: "editor-tokenVariable",
+ selector: "editor-tokenSelector",
+ string: "editor-tokenSelector",
+ symbol: "editor-tokenProperty",
+ tag: "editor-tokenProperty",
+ url: "editor-tokenOperator",
+ variable: "editor-tokenVariable",
+ },
+};
+
+// Simple Toolbar Plugin
+function ToolbarPlugin() {
+ const [editor] = useLexicalComposerContext();
+ const [isBold, setIsBold] = useState(false);
+ const [isItalic, setIsItalic] = useState(false);
+ const [isUnderline, setIsUnderline] = useState(false);
+ const [blockType, setBlockType] = useState("paragraph");
+
+ useEffect(() => {
+ return editor.registerUpdateListener(({ editorState }) => {
+ editorState.read(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ setIsBold(selection.hasFormat("bold"));
+ setIsItalic(selection.hasFormat("italic"));
+ setIsUnderline(selection.hasFormat("underline"));
+
+ const anchorNode = selection.anchor.getNode();
+ const element = anchorNode.getTopLevelElementOrThrow();
+ if ($isHeadingNode(element)) {
+ setBlockType(element.getTag());
+ } else {
+ setBlockType("paragraph");
+ }
+ }
+ });
+ });
+ }, [editor]);
+
+ const formatHeading = (headingSize: "h1" | "h2") => {
+ if (blockType !== headingSize) {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createHeadingNode(headingSize));
+ }
+ });
+ } else {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ }
+ });
+ }
+ };
+
+ const formatBulletList = () => {
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
+ };
+
+ const formatNumberedList = () => {
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Import HTML Plugin
+function HtmlImportPlugin({
+ initialHtml,
+ setEditor,
+}: {
+ initialHtml: string;
+ setEditor: (editor: LexicalEditor) => void;
+}) {
+ const [editor] = useLexicalComposerContext();
+
+ useEffect(() => {
+ console.log(
+ "HtmlImportPlugin effect running, editor:",
+ !!editor,
+ "initialHtml:",
+ initialHtml
+ ? initialHtml.substring(0, 50) + (initialHtml.length > 50 ? "..." : "")
+ : "empty"
+ );
+
+ if (editor) {
+ setEditor(editor);
+
+ editor.update(() => {
+ try {
+ if (initialHtml && initialHtml.trim() !== "") {
+ console.log("Importing HTML content into editor");
+ const parser = new DOMParser();
+ const dom = parser.parseFromString(initialHtml, "text/html");
+ const nodes = $generateNodesFromDOM(editor, dom);
+
+ const root = $getRoot();
+ root.clear();
+ $insertNodes(nodes);
+ console.log("HTML content imported successfully");
+ } else {
+ // If no initial content, ensure we have at least a paragraph
+ const root = $getRoot();
+ if (root.getChildrenSize() === 0) {
+ const paragraph = $createParagraphNode();
+ root.append(paragraph);
+ }
+ }
+ } catch (error) {
+ console.error("Error importing HTML content:", error);
+ // Ensure we have at least a paragraph on error
+ const root = $getRoot();
+ if (root.getChildrenSize() === 0) {
+ const paragraph = $createParagraphNode();
+ root.append(paragraph);
+ }
+ }
+ });
+ }
+ }, [editor, initialHtml, setEditor]);
+
+ return null;
+}
+
+export interface PolicyEditorProps {
+ initialContent: string;
+ onChange: (html: string) => void;
+}
+
+export default function PolicyEditor({
+ initialContent,
+ onChange,
+}: PolicyEditorProps) {
+ const [editor, setEditor] = useState(null);
+
+ console.log("PolicyEditor rendering with initialContent:", initialContent);
+
+ // Function to handle editor changes
+ const handleEditorChange = (editorState: EditorState) => {
+ if (!editor) return;
+
+ editorState.read(() => {
+ const htmlString = $generateHtmlFromNodes(editor);
+ console.log(
+ "Editor content changed:",
+ htmlString.substring(0, 100) + (htmlString.length > 100 ? "..." : "")
+ );
+ onChange(htmlString);
+ });
+ };
+
+ // Lexical Editor Configuration
+ const initialConfig = {
+ namespace: "PolicyEditor",
+ theme,
+ onError: (error: Error) => {
+ console.error("Lexical Editor Error:", error);
+ },
+ nodes: [
+ HeadingNode,
+ QuoteNode,
+ ListNode,
+ ListItemNode,
+ CodeNode,
+ CodeHighlightNode,
+ TableNode,
+ TableCellNode,
+ TableRowNode,
+ AutoLinkNode,
+ LinkNode,
+ ],
+ };
+
+ return (
+
+
+
+
+
+
+ }
+ placeholder={
+
+ Enter policy content...
+
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/console/src/components/editor/ToolbarPlugin.tsx b/apps/console/src/components/editor/ToolbarPlugin.tsx
new file mode 100644
index 000000000..28a2990a6
--- /dev/null
+++ b/apps/console/src/components/editor/ToolbarPlugin.tsx
@@ -0,0 +1,412 @@
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import {
+ $createParagraphNode,
+ $getSelection,
+ $isRangeSelection,
+ FORMAT_TEXT_COMMAND,
+ FORMAT_ELEMENT_COMMAND,
+ UNDO_COMMAND,
+ REDO_COMMAND,
+} from "lexical";
+import {
+ $createHeadingNode,
+ $createQuoteNode,
+ $isHeadingNode,
+} from "@lexical/rich-text";
+import {
+ $createListItemNode,
+ $createListNode,
+ $isListNode,
+ INSERT_ORDERED_LIST_COMMAND,
+ INSERT_UNORDERED_LIST_COMMAND,
+ ListNode,
+ REMOVE_LIST_COMMAND,
+} from "@lexical/list";
+import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
+import {
+ $getSelectionStyleValueForProperty,
+ $isParentElementRTL,
+ $patchStyleText,
+ $setBlocksType,
+} from "@lexical/selection";
+import { $getNearestNodeOfType } from "@lexical/utils";
+import { useCallback, useEffect, useState } from "react";
+import { createPortal } from "react-dom";
+import {
+ Bold,
+ Italic,
+ Underline,
+ Strikethrough,
+ List,
+ ListOrdered,
+ Link,
+ Quote,
+ Heading1,
+ Heading2,
+ Heading3,
+ Undo,
+ Redo,
+ AlignLeft,
+ AlignCenter,
+ AlignRight,
+ AlignJustify,
+} from "lucide-react";
+
+export function ToolbarPlugin() {
+ const [editor] = useLexicalComposerContext();
+ const [activeEditor, setActiveEditor] = useState(editor);
+ const [isBold, setIsBold] = useState(false);
+ const [isItalic, setIsItalic] = useState(false);
+ const [isUnderline, setIsUnderline] = useState(false);
+ const [isStrikethrough, setIsStrikethrough] = useState(false);
+ const [isLink, setIsLink] = useState(false);
+ const [blockType, setBlockType] = useState("paragraph");
+ const [selectedElementKey, setSelectedElementKey] = useState(
+ null
+ );
+ const [linkUrl, setLinkUrl] = useState("");
+ const [isLinkEditMode, setIsLinkEditMode] = useState(false);
+ const [textAlignment, setTextAlignment] = useState<
+ "left" | "center" | "right" | "justify"
+ >("left");
+
+ const updateToolbar = useCallback(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ setIsBold(selection.hasFormat("bold"));
+ setIsItalic(selection.hasFormat("italic"));
+ setIsUnderline(selection.hasFormat("underline"));
+ setIsStrikethrough(selection.hasFormat("strikethrough"));
+
+ // Check for links
+ const node = getSelectedNode(selection);
+ const parent = node.getParent();
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
+ setIsLink(true);
+ } else {
+ setIsLink(false);
+ }
+
+ // Get text alignment
+ const alignment = $getSelectionStyleValueForProperty(
+ selection,
+ "text-align",
+ "left"
+ ) as "left" | "center" | "right" | "justify";
+ setTextAlignment(alignment);
+
+ // Update block type
+ const anchorNode = selection.anchor.getNode();
+ const element =
+ anchorNode.getKey() === "root"
+ ? anchorNode
+ : anchorNode.getTopLevelElementOrThrow();
+ const elementKey = element.getKey();
+ const elementDOM = activeEditor.getElementByKey(elementKey);
+
+ setSelectedElementKey(elementKey);
+
+ if (elementDOM !== null) {
+ if ($isHeadingNode(element)) {
+ const tag = element.getTag();
+ setBlockType(tag);
+ } else if ($isListNode(element)) {
+ const parentList = $getNearestNodeOfType(
+ anchorNode,
+ ListNode
+ );
+ const listType = parentList
+ ? parentList.getListType()
+ : element.getListType();
+ setBlockType(listType);
+ } else {
+ setBlockType("paragraph");
+ }
+ }
+ }
+ }, [activeEditor]);
+
+ useEffect(() => {
+ return editor.registerUpdateListener(({ editorState }) => {
+ editorState.read(() => {
+ updateToolbar();
+ });
+ });
+ }, [editor, updateToolbar]);
+
+ const formatHeading = (headingSize: "h1" | "h2" | "h3") => {
+ if (blockType !== headingSize) {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createHeadingNode(headingSize));
+ }
+ });
+ } else {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ }
+ });
+ }
+ };
+
+ const formatQuote = () => {
+ if (blockType !== "quote") {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createQuoteNode());
+ }
+ });
+ } else {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ }
+ });
+ }
+ };
+
+ const formatBulletList = () => {
+ if (blockType !== "ul") {
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
+ } else {
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
+ }
+ };
+
+ const formatNumberedList = () => {
+ if (blockType !== "ol") {
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
+ } else {
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
+ }
+ };
+
+ const formatLink = () => {
+ if (!isLink) {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ const url = prompt("Enter URL", "https://");
+ if (url) {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
+ }
+ }
+ } else {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
+ }
+ };
+
+ const formatTextAlignment = (
+ alignment: "left" | "center" | "right" | "justify"
+ ) => {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $patchStyleText(selection, {
+ "text-align": alignment,
+ });
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function getSelectedNode(selection: any) {
+ const anchor = selection.anchor;
+ const focus = selection.focus;
+ const anchorNode = selection.anchor.getNode();
+ const focusNode = selection.focus.getNode();
+ if (anchorNode === focusNode) {
+ return anchorNode;
+ }
+ const isBackward = selection.isBackward();
+ if (isBackward) {
+ return $isAtNodeEnd(focus) ? anchorNode : focusNode;
+ } else {
+ return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
+ }
+}
+
+function $isAtNodeEnd(point: any) {
+ return point.offset === point.getNode().getTextContentSize();
+}
diff --git a/apps/console/src/pages/CreatePolicyPage.tsx b/apps/console/src/pages/CreatePolicyPage.tsx
index 14ed93c8c..13d296161 100644
--- a/apps/console/src/pages/CreatePolicyPage.tsx
+++ b/apps/console/src/pages/CreatePolicyPage.tsx
@@ -6,10 +6,10 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import { FileText } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import PolicyEditor from "@/components/PolicyEditor";
import type { CreatePolicyPageMutation } from "./__generated__/CreatePolicyPageMutation.graphql";
const CreatePolicyMutation = graphql`
@@ -39,6 +39,13 @@ export default function CreatePolicyPage() {
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
+ console.log(
+ "CreatePolicyPage state - content:",
+ content
+ ? content.substring(0, 50) + (content.length > 50 ? "..." : "")
+ : "empty"
+ );
+
const [commitMutation] =
useMutation(CreatePolicyMutation);
@@ -136,14 +143,12 @@ export default function CreatePolicyPage() {
diff --git a/apps/console/src/pages/PolicyOverviewPage.tsx b/apps/console/src/pages/PolicyOverviewPage.tsx
index 5366573b1..3ab70f1df 100644
--- a/apps/console/src/pages/PolicyOverviewPage.tsx
+++ b/apps/console/src/pages/PolicyOverviewPage.tsx
@@ -12,6 +12,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import type { PolicyOverviewPageQuery as PolicyOverviewPageQueryType } from "./__generated__/PolicyOverviewPageQuery.graphql";
import { Helmet } from "react-helmet-async";
+import "../styles/policy-content.css";
const PolicyOverviewPageQuery = graphql`
query PolicyOverviewPageQuery($policyId: ID!) {
@@ -102,8 +103,9 @@ function PolicyOverviewPageContent({
Policy Content
-
+
diff --git a/apps/console/src/pages/UpdatePolicyPage.tsx b/apps/console/src/pages/UpdatePolicyPage.tsx
index 87fcf2f62..98d473fc8 100644
--- a/apps/console/src/pages/UpdatePolicyPage.tsx
+++ b/apps/console/src/pages/UpdatePolicyPage.tsx
@@ -12,11 +12,11 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/hooks/use-toast";
import { FileText } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Suspense } from "react";
+import PolicyEditor from "@/components/PolicyEditor";
import type { UpdatePolicyPageQuery as UpdatePolicyPageQueryType } from "./__generated__/UpdatePolicyPageQuery.graphql";
import type { UpdatePolicyPageMutation as UpdatePolicyPageMutationType } from "./__generated__/UpdatePolicyPageMutation.graphql";
@@ -59,11 +59,20 @@ function UpdatePolicyPageContent({
queryRef
);
+ console.log("UpdatePolicyPage data:", data.node);
+
const [name, setName] = useState(data.node.name);
- const [content, setContent] = useState(data.node.content);
+ const [content, setContent] = useState(data.node.content || "");
const [status, setStatus] = useState(data.node.status);
const [isSubmitting, setIsSubmitting] = useState(false);
+ console.log(
+ "UpdatePolicyPage state - content:",
+ content
+ ? content.substring(0, 50) + (content.length > 50 ? "..." : "")
+ : "empty"
+ );
+
const [commitMutation] =
useMutation
(UpdatePolicyMutation);
@@ -152,14 +161,12 @@ function UpdatePolicyPageContent({
diff --git a/apps/console/src/styles/policy-content.css b/apps/console/src/styles/policy-content.css
new file mode 100644
index 000000000..67f21a66b
--- /dev/null
+++ b/apps/console/src/styles/policy-content.css
@@ -0,0 +1,127 @@
+/* Policy Content Styles */
+.policy-content {
+ line-height: 1.6;
+}
+
+.policy-content h1 {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-top: 1.5rem;
+ margin-bottom: 1rem;
+ color: hsl(var(--foreground));
+}
+
+.policy-content h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-top: 1.5rem;
+ margin-bottom: 0.75rem;
+ color: hsl(var(--foreground));
+}
+
+.policy-content h3 {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-top: 1.25rem;
+ margin-bottom: 0.75rem;
+ color: hsl(var(--foreground));
+}
+
+.policy-content h4 {
+ font-size: 1.125rem;
+ font-weight: 600;
+ margin-top: 1rem;
+ margin-bottom: 0.5rem;
+ color: hsl(var(--foreground));
+}
+
+.policy-content p {
+ margin-bottom: 1rem;
+}
+
+.policy-content ul,
+.policy-content ol {
+ margin-left: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.policy-content ul {
+ list-style-type: disc;
+}
+
+.policy-content ol {
+ list-style-type: decimal;
+}
+
+.policy-content li {
+ margin-bottom: 0.5rem;
+}
+
+.policy-content a {
+ color: hsl(var(--primary));
+ text-decoration: underline;
+}
+
+.policy-content a:hover {
+ text-decoration: none;
+}
+
+.policy-content blockquote {
+ border-left: 4px solid hsl(var(--border));
+ padding-left: 1rem;
+ margin-left: 0;
+ margin-right: 0;
+ font-style: italic;
+ color: hsl(var(--muted-foreground));
+}
+
+.policy-content code {
+ font-family: monospace;
+ background-color: hsl(var(--muted));
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.25rem;
+ font-size: 0.875em;
+}
+
+.policy-content pre {
+ background-color: hsl(var(--muted));
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow-x: auto;
+ margin-bottom: 1rem;
+}
+
+.policy-content pre code {
+ background-color: transparent;
+ padding: 0;
+ border-radius: 0;
+}
+
+.policy-content table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 1rem;
+}
+
+.policy-content th,
+.policy-content td {
+ border: 1px solid hsl(var(--border));
+ padding: 0.5rem;
+}
+
+.policy-content th {
+ background-color: hsl(var(--muted));
+ font-weight: 600;
+}
+
+.policy-content img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 0.5rem;
+}
+
+.policy-content hr {
+ border: 0;
+ border-top: 1px solid hsl(var(--border));
+ margin: 1.5rem 0;
+}
\ No newline at end of file