From 4a37d3b4cc51e100719a021f183394c470cbaeac Mon Sep 17 00:00:00 2001 From: polubis Date: Fri, 10 Oct 2025 14:37:43 +0200 Subject: [PATCH 01/23] wip --- jest.config.js | 1 + meta.ts | 3 + seo-plugins.ts | 1 + src/api-4markdown-contracts/atoms.ts | 7 + .../contracts/index.ts | 35 +++- .../dtos/access-group.dto.ts | 12 ++ src/api-4markdown-contracts/dtos/index.ts | 1 + src/components/user-popover-content.tsx | 22 +++ src/design-system/field.tsx | 29 ++- src/design-system/tabs-2.tsx | 65 +++++++ .../access-groups-management.view.tsx | 24 +++ .../acts/create-access-group.act.ts | 24 +++ .../acts/get-your-access-groups.act.ts | 29 +++ .../containers/groups-list.container.tsx | 92 +++++++++ .../containers/new-group.container.tsx | 180 ++++++++++++++++++ .../access-groups-management/store/actions.ts | 15 ++ .../access-groups-management/store/index.ts | 15 ++ .../access-groups-management/store/models.ts | 11 ++ src/libs/act/act.ts | 75 ++++++++ src/libs/act/index.ts | 3 + src/pages/access-groups.tsx | 41 ++++ tsconfig.json | 1 + 22 files changed, 683 insertions(+), 3 deletions(-) create mode 100644 src/api-4markdown-contracts/dtos/access-group.dto.ts create mode 100644 src/design-system/tabs-2.tsx create mode 100644 src/features/access-groups-management/access-groups-management.view.tsx create mode 100644 src/features/access-groups-management/acts/create-access-group.act.ts create mode 100644 src/features/access-groups-management/acts/get-your-access-groups.act.ts create mode 100644 src/features/access-groups-management/containers/groups-list.container.tsx create mode 100644 src/features/access-groups-management/containers/new-group.container.tsx create mode 100644 src/features/access-groups-management/store/actions.ts create mode 100644 src/features/access-groups-management/store/index.ts create mode 100644 src/features/access-groups-management/store/models.ts create mode 100644 src/libs/act/act.ts create mode 100644 src/libs/act/index.ts create mode 100644 src/pages/access-groups.tsx diff --git a/jest.config.js b/jest.config.js index eb5fddf20..4de3834f4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,6 +13,7 @@ module.exports = { "^layouts/(.*)": `/src/layouts/$1`, "^core/(.*)": `/src/core/$1`, "^components/(.*)": `/src/components/$1`, + "^libs/(.*)": `/src/libs/$1`, "^providers/(.*)": `/src/providers/$1`, "^containers/(.*)": `/src/containers/$1`, "^design-system/(.*)": `/src/design-system/$1`, diff --git a/meta.ts b/meta.ts index 961315862..cda83df13 100644 --- a/meta.ts +++ b/meta.ts @@ -20,6 +20,9 @@ export const meta = { ytVideoTutorialUrl: `https://www.youtube.com/watch?v=t3Ve0em65rY`, mdCheatsheet: `https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet`, routes: { + accessGroups: { + management: `/access-groups/`, + }, mindmaps: { mindmap: `/mindmap/`, creator: `/mindmap-creator/`, diff --git a/seo-plugins.ts b/seo-plugins.ts index 575158524..7188f0952 100644 --- a/seo-plugins.ts +++ b/seo-plugins.ts @@ -30,6 +30,7 @@ const disallowedPaths = [ meta.routes.creator.preview, meta.routes.sandbox, meta.routes.mindmaps.preview, + meta.routes.accessGroups.management, legacyRoutes.documents.preview, legacyRoutes.documents.browse, ]; diff --git a/src/api-4markdown-contracts/atoms.ts b/src/api-4markdown-contracts/atoms.ts index 2386c6afa..30a95dad4 100644 --- a/src/api-4markdown-contracts/atoms.ts +++ b/src/api-4markdown-contracts/atoms.ts @@ -5,6 +5,8 @@ type Id = string; type Name = string; type MarkdownCode = string; type Date = string; +type UTCDate = Brand; +type Etag = Brand; type Tags = string[]; type Path = string; type MarkdownContent = string; @@ -19,6 +21,8 @@ type DocumentId = Brand; type MindmapNodeId = Brand; type MindmapId = Brand; +type AccessGroupId = Brand; + type ResourceId = DocumentId | MindmapNodeId | MindmapId; const RESOURCE_TYPES = ["document", "mindmap", "mindmap-node"] as const; @@ -45,4 +49,7 @@ export type { MindmapId, MindmapNodeId, ResourceType, + AccessGroupId, + Etag, + UTCDate, }; diff --git a/src/api-4markdown-contracts/contracts/index.ts b/src/api-4markdown-contracts/contracts/index.ts index 679e19cdf..b5544a0f0 100644 --- a/src/api-4markdown-contracts/contracts/index.ts +++ b/src/api-4markdown-contracts/contracts/index.ts @@ -1,8 +1,10 @@ import { Brand, type Prettify } from "development-kit/utility-types"; import type { + AccessGroupId, Base64, Date, DocumentId, + Etag, Id, MindmapId, MindmapNodeId, @@ -27,6 +29,7 @@ import type { CommentDto, ResourceCompletionDto, } from "../dtos"; +import { AccessGroupDto } from "../dtos/access-group.dto"; // @TODO[PRIO=1]: [Add better error handling and throwing custom errors]. type Contract = { @@ -269,6 +272,33 @@ type SetUserResourceCompletionContract = Contract< } >; +type GetYourAccessGroupsContract = Contract< + "getYourAccessGroups", + { + hasMore: boolean; + nextAccessGroupId: AccessGroupId | null; + accessGroups: AccessGroupDto[]; + }, + { limit: number | null; nextAccessGroupId: AccessGroupId | null } +>; + +type CreateAccessGroupContract = Contract< + "createAccessGroup", + AccessGroupDto, + { name: string; description: string | null } +>; + +type EditAccessGroupContract = Contract< + "editAccessGroup", + AccessGroupDto, + { + name: string; + description: string | null; + accessGroupId: AccessGroupId; + etag: Etag; + } +>; + type API4MarkdownContracts = | CreateMindmapContract | GetYourDocumentsContract @@ -298,7 +328,10 @@ type API4MarkdownContracts = | GetUserProfileContract | AddUserProfileCommentContract | GetUserResourceCompletionsContract - | SetUserResourceCompletionContract; + | SetUserResourceCompletionContract + | GetYourAccessGroupsContract + | CreateAccessGroupContract + | EditAccessGroupContract; type API4MarkdownContractKey = API4MarkdownContracts["key"]; type API4MarkdownDto = Extract< diff --git a/src/api-4markdown-contracts/dtos/access-group.dto.ts b/src/api-4markdown-contracts/dtos/access-group.dto.ts new file mode 100644 index 000000000..02eea7dcb --- /dev/null +++ b/src/api-4markdown-contracts/dtos/access-group.dto.ts @@ -0,0 +1,12 @@ +import { AccessGroupId, Etag, UTCDate } from "../atoms"; + +type AccessGroupDto = { + id: AccessGroupId; + cdate: UTCDate; + etag: Etag; + mdate: UTCDate; + name: string; + description: string | null; +}; + +export type { AccessGroupDto }; diff --git a/src/api-4markdown-contracts/dtos/index.ts b/src/api-4markdown-contracts/dtos/index.ts index 227523f66..af579ce37 100644 --- a/src/api-4markdown-contracts/dtos/index.ts +++ b/src/api-4markdown-contracts/dtos/index.ts @@ -8,3 +8,4 @@ export * from "./rewrite-assistant.dto"; export * from "./your-account.dto"; export * from "./comment.dto"; export * from "./resource-completion.dto"; +export * from "./access-group.dto"; diff --git a/src/components/user-popover-content.tsx b/src/components/user-popover-content.tsx index 08c9c3f69..5797f2525 100644 --- a/src/components/user-popover-content.tsx +++ b/src/components/user-popover-content.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Button } from "design-system/button"; import { + BiArrowToRight, BiPencil, BiRefresh, BiSolidUserDetail, @@ -74,6 +75,27 @@ const UserPopoverContent = ({ onClose }: { onClose(): void }) => { {yourUserProfile.is === `ok` && ( <> +
+
+ +
+
Your Access Groups
+

+ Engage your audience and assign them to access groups to share + specific materials. +

+
+ {yourUserProfile.user?.displayName && yourUserProfile.user?.bio ? ( <>
{ return ( -
+
{label && } {children} {hint} -
+
+ ); +}; + +type HintProps = React.ComponentPropsWithoutRef<"i">; + +const Hint = ({ children, className, ...props }: HintProps) => { + return ( + + {children} + + ); +}; + +type ErrorProps = React.ComponentPropsWithoutRef<"i">; + +const Error = ({ children, className, ...props }: ErrorProps) => { + return ( + + {children} + ); }; @@ -39,5 +62,7 @@ const Label = ({ }; Field.Label = Label; +Field.Hint = Hint; +Field.Error = Error; export { Field }; diff --git a/src/design-system/tabs-2.tsx b/src/design-system/tabs-2.tsx new file mode 100644 index 000000000..fa4e9af8b --- /dev/null +++ b/src/design-system/tabs-2.tsx @@ -0,0 +1,65 @@ +import React, { ButtonHTMLAttributes } from "react"; +import { c } from "./c"; + +interface Tabs2Props { + children: React.ReactNode; + className?: string; +} + +const Tabs2 = ({ children, className }: Tabs2Props) => { + return ( +
*:first-child]:rounded-s-md [&>*:last-child]:rounded-r-md", + className, + )} + > + {children} +
+ ); +}; + +type Tabs2ItemProps = { + active?: boolean; + className?: string; + children: React.ReactNode; +} & ButtonHTMLAttributes; + +const Tabs2Item = ({ active, className, ...props }: Tabs2ItemProps) => { + return ( + + + ); + } + + if (accessGroups.length === 0) { + return ( +
+

No access groups found

+
+ ); + } + + return ( +
+

Access groups

+
+
    + {accessGroups.map((accessGroup) => ( +
  • +

    + {accessGroup.name} +

    +
  • + ))} +
+
+
+ ); +}; + +const GroupsListContainer = () => { + useEffect(() => { + getYourAccessGroupsAct(); + }, []); + + return ( + <> +
+

Access groups

+ +
+
+ +
+ + ); +}; + +export { GroupsListContainer }; diff --git a/src/features/access-groups-management/containers/new-group.container.tsx b/src/features/access-groups-management/containers/new-group.container.tsx new file mode 100644 index 000000000..1b98a7fbc --- /dev/null +++ b/src/features/access-groups-management/containers/new-group.container.tsx @@ -0,0 +1,180 @@ +import { Button } from "design-system/button"; +import { Field } from "design-system/field"; +import { Input } from "design-system/input"; +import { Textarea } from "design-system/textarea"; +import { useForm } from "development-kit/use-form"; +import React from "react"; +import { ValidatorFn, ValidatorsSetup } from "development-kit/form"; +import { changeViewAction } from "../store/actions"; +import { BiPlus } from "react-icons/bi"; +import { useAct } from "libs/act"; +import { createAccessGroupAct } from "../acts/create-access-group.act"; + +const validationLimits = { + name: { + min: 2, + max: 100, + }, + description: { + min: 10, + max: 500, + }, +} as const; + +const groupNameValidator: ValidatorFn = (value: string) => { + const trimmed = value.trim(); + + if (trimmed.length < validationLimits.name.min) { + return `Group name must be at least ${validationLimits.name.min} characters long`; + } + + if (trimmed.length > validationLimits.name.max) { + return `Group name must be at most ${validationLimits.name.max} characters long`; + } + + if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) { + return `Group name can only contain letters, numbers, underscores, and dashes`; + } + + return null; +}; + +const groupDescriptionValidator: ValidatorFn = ( + value: string, +) => { + const trimmed = value.trim(); + + if (trimmed.length === 0) { + return null; + } + + if (trimmed.length < validationLimits.description.min) { + return `Group description must be at least ${validationLimits.description.min} characters long`; + } + + if (trimmed.length > validationLimits.description.max) { + return `Group description must be at most ${validationLimits.description.max} characters long`; + } + + return null; +}; + +type FormValues = { + name: string; + description: string; +}; + +const validators: ValidatorsSetup = { + name: [groupNameValidator], + description: [groupDescriptionValidator], +}; + +const NewGroupContainer = () => { + const [{ invalid, values, untouched, result }, { inject }] = + useForm( + { + name: "", + description: "", + }, + validators, + ); + + const { name, description } = values; + + const [, { busy }, start] = useAct({ + onOk: () => changeViewAction("list"), + }); + + const handleSubmit = () => { + start(() => + createAccessGroupAct({ + name, + description: description || null, + }), + ); + }; + + return ( +
+
+

Create New Group

+

+ You'll be able to manage access to your created content. After this + step you can add users to the group. +

+
+
+
+ {result.name} + ) : ( + + {validationLimits.name.min}-{validationLimits.name.max}{" "} + characters. Only letters, numbers, underscores, and dashes + allowed. + + ) + } + > + + + + {result.description} + ) : ( + + Optional. {validationLimits.description.min}- + {validationLimits.description.max} characters if provided. + + ) + } + > +