diff --git a/frontend/packages/app/package.json b/frontend/packages/app/package.json index 4f4f938f3a..d15a41e9dd 100644 --- a/frontend/packages/app/package.json +++ b/frontend/packages/app/package.json @@ -19,6 +19,7 @@ "@emotion/serialize": "1.1.2", "@emotion/utils": "1.2.1", "@floating-ui/react": "0.25.1", + "@hookform/resolvers": "^3.3.1", "@juggle/resize-observer": "3.4.0", "@mantine/core": "6.0.13", "@mantine/hooks": "6.0.13", @@ -100,6 +101,7 @@ "react-dom": "18.2.0", "react-error-boundary": "4.0.11", "react-highlight-words": "0.20.0", + "react-hook-form": "^7.46.1", "react-hot-toast": "2.4.1", "react-icons": "4.9.0", "react-native": "0.71.7", @@ -128,7 +130,8 @@ "vite": "4.4.9", "y-prosemirror": "1.2.1", "y-protocols": "1.0.5", - "yjs": "13.6.4" + "yjs": "13.6.4", + "zod": "^3.22.2" }, "devDependencies": { "@babel/core": "7.22.10", diff --git a/frontend/packages/app/src/components/dialog.tsx b/frontend/packages/app/src/components/dialog.tsx index 40adedfedf..0a1d6a2e47 100644 --- a/frontend/packages/app/src/components/dialog.tsx +++ b/frontend/packages/app/src/components/dialog.tsx @@ -64,7 +64,7 @@ export function AppDialog({ isAlert, }: { TriggerComponent: React.FC - ContentComponent: React.FC<{onClose?: () => void}> + ContentComponent: React.FC<{onClose?: () => void; isOpen?: boolean}> isAlert?: boolean }) { const Component = getComponent(isAlert) @@ -85,6 +85,7 @@ export function AppDialog({ /> { setIsOpen(false) }} diff --git a/frontend/packages/app/src/components/list-item.tsx b/frontend/packages/app/src/components/list-item.tsx index 6582c60017..6fceb499d9 100644 --- a/frontend/packages/app/src/components/list-item.tsx +++ b/frontend/packages/app/src/components/list-item.tsx @@ -42,6 +42,7 @@ export function ListItem({ onPointerEnter={onPointerEnter} // onPointerLeave={() => setIsHovering(false)} chromeless + onPress={onPress} > - @@ -70,7 +106,7 @@ function NewGroupButton(props: React.ComponentProps) { } export function AddGroupButton() { return ( - + void openRoute: NavRoute + onLabelPress?: () => void }) { const spawn = useNavigate('spawn') const title = getDocumentTitle(publication.document) @@ -75,7 +77,23 @@ export function PublicationListItem({ )} {label && ( - + { + if (onLabelPress) { + e.stopPropagation() + onLabelPress() + } + }} + hoverStyle={ + onLabelPress + ? { + textDecorationLine: 'underline', + } + : undefined + } + > {label} )} diff --git a/frontend/packages/app/src/components/titlebar/publish-share.tsx b/frontend/packages/app/src/components/titlebar/publish-share.tsx index 8dc1860dfd..264f7db277 100644 --- a/frontend/packages/app/src/components/titlebar/publish-share.tsx +++ b/frontend/packages/app/src/components/titlebar/publish-share.tsx @@ -1,5 +1,9 @@ import {useMyAccount} from '@mintter/app/models/accounts' -import {usePublication, usePublishDraft} from '@mintter/app/models/documents' +import { + useDraftTitle, + usePublication, + usePublishDraft, +} from '@mintter/app/models/documents' import { useAccountGroups, useDocumentGroups, @@ -39,7 +43,6 @@ import { SizableText, Spinner, Text, - View, XStack, YStack, styled, @@ -48,17 +51,18 @@ import { Album, Book, CheckCheck, - CheckCircle, ChevronDown, ChevronUp, - Folder, Pencil, X, } from '@tamagui/lucide-icons' -import {ReactNode, useEffect, useMemo, useState} from 'react' +import {useEffect, useMemo, useState} from 'react' import toast from 'react-hot-toast' import DiscardDraftButton from './discard-draft-button' -import {ListDocumentGroupsResponse_Item} from 'frontend/packages/shared/src/client/.generated/groups/v1alpha/groups_pb' +import {pathNameify} from '@mintter/app/utils/path' +import {useAppDialog} from '../dialog' +import {RenamePubDialog} from '@mintter/app/pages/group' +import {usePublicationInContext} from '@mintter/app/models/publication' // function DraftPublicationDialog({ // draft, @@ -118,6 +122,7 @@ function GroupPublishDialog({ docId: string version: string | undefined editDraftId?: string | undefined + docTitle: string | undefined } dialogState: DialogProps }) { @@ -134,7 +139,8 @@ function GroupPublishDialog({ if (myGroups?.length && !selectedGroupId) setSelectedGroupId(myGroups[0]?.id) }, [myGroups, selectedGroupId]) - const [pathName, setPathName] = useState(input.docId) + const defaultPathName = pathNameify(input.docTitle || '') + const [pathName, setPathName] = useState(defaultPathName) const route = useNavRoute() const navigate = useNavigate('replace') const draftRoute = route.key === 'draft' ? route : null @@ -194,10 +200,6 @@ function GroupPublishDialog({ }} > Publish to Group -
- - -
@@ -251,6 +253,11 @@ function GroupPublishDialog({
+
+ + +
+ @@ -291,73 +298,6 @@ const ContextPopoverArrow = styled(Popover.Arrow, { borderColor: '$borderColor', }) -// function ContextDropdown({dropdown, trigger}: {dropdown :ReactNode, trigger: ReactNode}) { -// return -// -// -// -// -// -// -// {draftRoute && !groupPubContext ? ( -// <> -// -// Will Publish on the Public Web -// -// -// ) : null} -// {publishedGroups?.items.length ? ( -// -// -// Published to: -// -// {publishedGroups.items.map((g) => ( -// -// ))} -// -// ) : null} -// {groupPubContext ? ( -// <> -// -// -// ) : null} -// {documentId && (pubRoute || !groupPubContext) ? ( -// popoverState.onOpenChange(false)} -// /> -// ) : null} -// -// -// -// } - function GroupContextButton({route}: {route: GroupRoute}) { const group = useGroup(route.groupId) if (!group.data) return null @@ -373,6 +313,7 @@ function DraftContextButton({route}: {route: DraftRoute}) { const account = useMyAccount() const nav = useNavigate('replace') const groups = useAccountGroups(account?.data?.id) + const draftTitle = useDraftTitle({documentId: route.draftId}) const groupPubContext = route.pubContext?.key === 'group' ? route.pubContext : null const selectedGroup = groupPubContext @@ -389,6 +330,11 @@ function DraftContextButton({route}: {route: DraftRoute}) { title = selectedGroup.group?.title || '' } const [isListingGroups, setIsListingGroups] = useState(false) + let displayPathName = + route.pubContext?.key === 'group' ? route.pubContext?.pathName : undefined + if (!displayPathName && draftTitle) { + displayPathName = pathNameify(draftTitle) + } return ( @@ -403,10 +349,9 @@ function DraftContextButton({route}: {route: DraftRoute}) { <> Committing to Group: {}} route={route} /> @@ -470,22 +415,29 @@ function DraftContextButton({route}: {route: DraftRoute}) { } function GroupContextItem({ - docGroup, + groupId, + groupVersion, + path, route, + onPathPress, }: { - docGroup: ListDocumentGroupsResponse_Item - route: PublicationRoute + groupId: string + groupVersion?: string | undefined + path: string | null + route: PublicationRoute | DraftRoute + onPathPress?: (() => void) | undefined }) { const replaceRoute = useNavigate('replace') - const group = useGroup(docGroup.groupId) + const group = useGroup(groupId) const isActive = route.pubContext?.key === 'group' && - docGroup.groupId === route.pubContext.groupId && - docGroup.path === route.pubContext.pathName + groupId === route.pubContext.groupId && + path === route.pubContext.pathName const myGroups = useMyGroups() const isGroupMember = myGroups.data?.items?.find((groupAccount) => { - return groupAccount.group?.id === docGroup.groupId + return groupAccount.group?.id === groupId }) + const isPathPressable = isActive && isGroupMember && onPathPress return ( - {isGroupMember ? ( - - - - - - - - {docGroups.data?.length ? ( - <> - Appears in: - - {docGroups.data?.map((docGroup) => { - return ( - - ) - })} - - - ) : null} - - - - + <> + + + + + + + + + + {docGroups.data?.length ? ( + <> + Appears in: + + {docGroups.data?.map((docGroup) => { + return ( + { + renameDialog.open({ + groupId: docGroup.groupId, + pathName: docGroup.path, + docTitle: publication.data?.document?.title || '', + }) + }} + key={`${docGroup.groupId}-${docGroup.path}`} + /> + ) + })} + + + ) : null} + + + + + {renameDialog.content} + ) } @@ -648,12 +637,14 @@ function PublishDialogInstance({ version, editDraftId, groupPubContext, + docTitle, ...props }: DialogProps & { closePopover?: () => void docId: string version: string | undefined editDraftId?: string | undefined + docTitle?: string | undefined groupPubContext: GroupPublicationRouteContext | null }) { const nav = useNavigation() @@ -700,7 +691,7 @@ function PublishDialogInstance({ > diff --git a/frontend/packages/app/src/components/titlebar/title.tsx b/frontend/packages/app/src/components/titlebar/title.tsx index 09504b67c7..a2feaa6c8e 100644 --- a/frontend/packages/app/src/components/titlebar/title.tsx +++ b/frontend/packages/app/src/components/titlebar/title.tsx @@ -16,7 +16,7 @@ import { User, XStack, } from '@mintter/ui' -import {Contact, Folder, Library} from '@tamagui/lucide-icons' +import {Bookmark, Contact, Folder, Library} from '@tamagui/lucide-icons' import {getDocumentTitle} from '../publication-list-item' import {useEffect} from 'react' import {NavRoute} from '../../utils/navigation' @@ -43,9 +43,9 @@ export function TitleContent({size = '$4'}: {size?: FontSizeTokens}) { if (route.key === 'home') { return ( <> - + - Trusted Publications + Publications ) diff --git a/frontend/packages/app/src/models/documents.ts b/frontend/packages/app/src/models/documents.ts index 64df017d62..52cee8f73f 100644 --- a/frontend/packages/app/src/models/documents.ts +++ b/frontend/packages/app/src/models/documents.ts @@ -53,6 +53,7 @@ import {formattingToolbarFactory} from '../editor/formatting-toolbar' import {PublicationRouteContext, useNavRoute} from '../utils/navigation' import {queryKeys} from './query-keys' import {usePublicationInContext} from './publication' +import {pathNameify} from '../utils/path' export type HMBlock = Block export type HMPartialBlock = PartialBlock @@ -264,6 +265,7 @@ export function usePublishDraft( } >, ) { + const queryClient = useAppContext().queryClient const grpcClient = useGRPCClient() const route = useNavRoute() const draftRoute = route.key === 'draft' ? route : undefined @@ -279,16 +281,26 @@ export function usePublishDraft( const pub = await grpcClient.drafts.publishDraft({documentId: draftId}) const publishedId = pub.document?.id if (draftGroupContext && publishedId) { - let publishPathName = - draftGroupContext.pathName === '' - ? publishedId - : draftGroupContext.pathName - await grpcClient.groups.updateGroup({ - id: draftGroupContext.groupId, - updatedContent: { - [publishPathName]: `${HYPERMEDIA_DOCUMENT_PREFIX}${publishedId}?v=${pub.version}`, - }, - }) + console.log('=== ayyho') + let docTitle: string | undefined = ( + queryClient.client.getQueryData([ + queryKeys.EDITOR_DRAFT, + draftId, + ]) as any + )?.title + console.log('=== ayyho') + let fallbackTitle = docTitle ? docTitle : publishedId.slice(0, 5) + let publishPathName = draftGroupContext.pathName + ? fallbackTitle + : draftGroupContext.pathName + if (publishPathName) { + await grpcClient.groups.updateGroup({ + id: draftGroupContext.groupId, + updatedContent: { + [publishPathName]: `${HYPERMEDIA_DOCUMENT_PREFIX}${publishedId}?v=${pub.version}`, + }, + }) + } } return pub }, diff --git a/frontend/packages/app/src/models/groups.ts b/frontend/packages/app/src/models/groups.ts index c0b19accdc..9fe639f292 100644 --- a/frontend/packages/app/src/models/groups.ts +++ b/frontend/packages/app/src/models/groups.ts @@ -1,4 +1,4 @@ -import {HYPERMEDIA_DOCUMENT_PREFIX, Role} from '@mintter/shared' +import {HYPERMEDIA_DOCUMENT_PREFIX, Role, getIdsfromUrl} from '@mintter/shared' import {UseMutationOptions, useMutation, useQuery} from '@tanstack/react-query' import {useGRPCClient, useQueryInvalidator} from '../app-context' import {queryKeys} from './query-keys' @@ -171,7 +171,7 @@ type RenameGroupDocMutationInput = { } export function useRenameGroupDoc( - opts?: UseMutationOptions, + opts?: UseMutationOptions, ) { const grpcClient = useGRPCClient() const invalidate = useQueryInvalidator() @@ -184,6 +184,13 @@ export function useRenameGroupDoc( const listed = await grpcClient.groups.listContent({ id: groupId, }) + console.log( + 'huh RENAME?!', + groupId, + pathName, + newPathName, + listed.content, + ) const prevPathValue = listed.content[pathName] if (!prevPathValue) throw new Error('Could not find previous path at ' + pathName) @@ -191,20 +198,26 @@ export function useRenameGroupDoc( id: groupId, updatedContent: {[pathName]: '', [newPathName]: prevPathValue}, }) + return prevPathValue }, onSuccess: (result, input, context) => { + const [docId] = getIdsfromUrl(result) opts?.onSuccess?.(result, input, context) invalidate([queryKeys.GET_GROUP_CONTENT, input.groupId]) + invalidate([queryKeys.GET_GROUPS_FOR_DOCUMENT, docId]) }, }) } -export function useGroupContent(groupId?: string | undefined) { +export function useGroupContent( + groupId?: string | undefined, + version?: string, +) { const grpcClient = useGRPCClient() return useQuery({ - queryKey: [queryKeys.GET_GROUP_CONTENT, groupId], + queryKey: [queryKeys.GET_GROUP_CONTENT, groupId, version], queryFn: async () => { - return await grpcClient.groups.listContent({id: groupId}) + return await grpcClient.groups.listContent({id: groupId, version}) }, enabled: !!groupId, }) diff --git a/frontend/packages/app/src/models/publication.ts b/frontend/packages/app/src/models/publication.ts index 70d2957e24..bc1c225de7 100644 --- a/frontend/packages/app/src/models/publication.ts +++ b/frontend/packages/app/src/models/publication.ts @@ -16,9 +16,12 @@ export function usePublicationInContext({ }) { const groupContext = pubContext?.key === 'group' ? pubContext : undefined const groupContextId = groupContext ? groupContext.groupId : undefined - console.log('sooo useGroupContent', groupContextId) - const groupContent = useGroupContent(groupContextId) + const groupContextVersion = groupContext + ? groupContext.groupVersion + : undefined + const groupContent = useGroupContent(groupContextId, groupContextVersion) let queryVersionId = versionId + let queryDocumentId = documentId const groupContextContent = groupContent.data?.content if ( !queryVersionId && @@ -28,10 +31,10 @@ export function usePublicationInContext({ ) { const contentURL = groupContextContent[groupContext.pathName] if (!contentURL) { - console.error(groupContextContent) - throw new Error( - `Group ${groupContextId} does not contain path "${groupContext.pathName}"`, - ) + // throw new Error( + // `Group ${groupContextId} does not contain path "${groupContext.pathName}"`, + // ) + queryDocumentId = undefined } const [groupContentDocId, groupContentVersion] = getIdsfromUrl(contentURL) if (groupContentDocId !== documentId) @@ -44,7 +47,7 @@ export function usePublicationInContext({ const pubQueryReady = !!queryVersionId || pubContext?.key !== 'group' return usePublication({ ...options, - documentId, + documentId: queryDocumentId, versionId: queryVersionId, enabled: options.enabled !== false && pubQueryReady, }) diff --git a/frontend/packages/app/src/pages/group.tsx b/frontend/packages/app/src/pages/group.tsx index db5f8a85f3..8be22b25b3 100644 --- a/frontend/packages/app/src/pages/group.tsx +++ b/frontend/packages/app/src/pages/group.tsx @@ -32,8 +32,9 @@ import { } from '../models/groups' import {useNavRoute} from '../utils/navigation' import {AppLinkText} from '../components/link' +import {pathNameify} from '../utils/path' -function RenamePubDialog({ +export function RenamePubDialog({ input: {groupId, pathName, docTitle}, onClose, }: { @@ -47,7 +48,11 @@ function RenamePubDialog({ onSubmit={() => { onClose() toast.promise( - renameDoc.mutateAsync({pathName, groupId, newPathName: renamed}), + renameDoc.mutateAsync({ + pathName, + groupId, + newPathName: pathNameify(renamed), + }), { success: 'Renamed', loading: 'Renaming..', @@ -61,7 +66,18 @@ function RenamePubDialog({ Choose a new short name for "{docTitle}" in this group. Be careful, as this will change web URLs. - + { + setRenamed( + value + .toLocaleLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-_]/g, '') + .replace(/-{2,}/g, '-'), + ) + }} + /> @@ -92,6 +108,13 @@ function GroupContentItem({ publication={pub.data} hasDraft={hasDraft} label={pathName} + onLabelPress={() => { + renameDialog.open({ + pathName, + groupId, + docTitle: pub.data.document?.title || '', + }) + }} pubContext={{key: 'group', groupId, pathName}} menuItems={[ { diff --git a/frontend/packages/app/src/utils/navigation.tsx b/frontend/packages/app/src/utils/navigation.tsx index 9d1fb02cf8..7e79b53de1 100644 --- a/frontend/packages/app/src/utils/navigation.tsx +++ b/frontend/packages/app/src/utils/navigation.tsx @@ -26,7 +26,8 @@ type PublicationCommentsAccessory = {key: 'comments'} export type GroupPublicationRouteContext = { key: 'group' groupId: string - pathName: string + groupVersion?: string | undefined + pathName: string | null } export type PublicationRouteContext = | null diff --git a/frontend/packages/app/src/utils/open-draft.ts b/frontend/packages/app/src/utils/open-draft.ts index c517c4065a..4d38d10590 100644 --- a/frontend/packages/app/src/utils/open-draft.ts +++ b/frontend/packages/app/src/utils/open-draft.ts @@ -34,12 +34,19 @@ export function useOpenDraft() { newWindow = true, pubContext?: PublicationRouteContext | undefined, ) { + const destPubContext: PublicationRouteContext = + pubContext?.key === 'group' + ? { + ...pubContext, + pathName: null, + } + : pubContext || null createDraft(grpcClient, pubContext) .then((docId: string) => { const draftRoute: DraftRoute = { key: 'draft', draftId: docId, - pubContext, + pubContext: destPubContext, contextRoute: route, } invalidate([queryKeys.GET_DRAFT_LIST]) diff --git a/frontend/packages/app/src/utils/path.ts b/frontend/packages/app/src/utils/path.ts new file mode 100644 index 0000000000..e6b08fc8b8 --- /dev/null +++ b/frontend/packages/app/src/utils/path.ts @@ -0,0 +1,8 @@ +export function pathNameify(name: string) { + return name + .trim() + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-_]/g, '') + .replace(/-{2,}/g, '-') +} diff --git a/yarn.lock b/yarn.lock index 2cc6d0ef71..0f1b7ba66a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4307,6 +4307,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:^3.3.1": + version: 3.3.1 + resolution: "@hookform/resolvers@npm:3.3.1" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 1ddc250a8d6769fb11b03110b586677b03463276dda1cdfd0225ab94f46d422868f74b01bef85f785010cc3d836f0669d6b6c0ed752cae532d2badf3537b1e72 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.11 resolution: "@humanwhocodes/config-array@npm:0.11.11" @@ -4691,6 +4700,7 @@ __metadata: "@graphql-codegen/cli": 2.16.5 "@graphql-codegen/typescript": 2.8.5 "@happy-dom/global-registrator": 7.8.1 + "@hookform/resolvers": ^3.3.1 "@juggle/resize-observer": 3.4.0 "@mantine/core": 6.0.13 "@mantine/hooks": 6.0.13 @@ -4799,6 +4809,7 @@ __metadata: react-dom: 18.2.0 react-error-boundary: 4.0.11 react-highlight-words: 0.20.0 + react-hook-form: ^7.46.1 react-hot-toast: 2.4.1 react-icons: 4.9.0 react-native: 0.71.7 @@ -4834,6 +4845,7 @@ __metadata: y-prosemirror: 1.2.1 y-protocols: 1.0.5 yjs: 13.6.4 + zod: ^3.22.2 languageName: unknown linkType: soft @@ -25731,6 +25743,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.46.1": + version: 7.46.1 + resolution: "react-hook-form@npm:7.46.1" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: 9c11ba454ce5b2a16e7499f2ca710e0c0cff227f39689b8e7985902f27f99bfc7a2c49f145ef228528764cf8286bf6101fc8a0f5094ce652eb31cab1684518d1 + languageName: node + linkType: hard + "react-hot-toast@npm:2.4.1": version: 2.4.1 resolution: "react-hot-toast@npm:2.4.1" @@ -31918,7 +31939,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:3.22.2": +"zod@npm:3.22.2, zod@npm:^3.22.2": version: 3.22.2 resolution: "zod@npm:3.22.2" checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9