Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { SelectAction, useActionGating } from '@hypha-platform/epics';
import { Locale } from '@hypha-platform/i18n';
import { isAbsoluteUrl } from '@hypha-platform/ui-utils';
import {
ArchiveIcon,
ArrowDownIcon,
ArrowLeftIcon,
ArrowRightIcon,
Expand Down Expand Up @@ -68,16 +67,6 @@ export const SelectSettingsAction = ({
baseTab: 'agreements',
icon: <ArrowRightIcon />,
},
{
group: 'Overview',
title: 'Archive Space (Coming Soon)',
description:
'Archive this space to disable activity while preserving its data and history.',
href: '#',
icon: <ArchiveIcon />,
baseTab: 'members',
disabled: true,
},
{
defaultDurationDays: 4,
group: 'Agreements',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/[lang]/dho/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
DEFAULT_SPACE_LEAD_IMAGE,
fetchSpaceDetails,
fetchSpaceProposalsIds,
isSpaceArchived,
} from '@hypha-platform/core/client';
import { notFound } from 'next/navigation';
import { db } from '@hypha-platform/storage-postgres';
Expand Down Expand Up @@ -165,6 +166,9 @@ export default async function DhoLayout({
web3SpaceId={spaceFromDb.web3SpaceId as number}
isSandbox={spaceFromDb.flags.includes('sandbox')}
isDemo={spaceFromDb.flags.includes('demo')}
isArchived={
spaceFromDb.flags.includes('archived') || spaceMembers === 0
}
configPath={`${getDhoPathAgreements(
lang,
daoSlug,
Expand Down Expand Up @@ -202,6 +206,7 @@ export default async function DhoLayout({
title={space.title as string}
isSandbox={space.flags?.includes('sandbox') ?? false}
isDemo={space.flags?.includes('demo') ?? false}
isArchived={isSpaceArchived(space)}
web3SpaceId={space.web3SpaceId as number}
configPath={`${getDhoPathAgreements(
lang,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/[lang]/my-spaces/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SpaceSearch,
AuthenticatedLinkButton,
} from '@hypha-platform/epics';
import { isSpaceArchived } from '@hypha-platform/core/client';
import Link from 'next/link';
import { Locale } from '@hypha-platform/i18n';
import {
Expand Down Expand Up @@ -91,6 +92,7 @@ export default async function Index(props: PageProps) {
title={space.title as string}
isSandbox={space.flags?.includes('sandbox') ?? false}
isDemo={space.flags?.includes('demo') ?? false}
isArchived={isSpaceArchived(space)}
web3SpaceId={space.web3SpaceId as number}
createdAt={space.createdAt}
configPath={`${getDhoPathAgreements(
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/[lang]/network/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default async function Index(props: PageProps) {
const spaces = await getAllSpaces({
search: query?.trim() || undefined,
parentOnly: false,
omitArchived: true,
});

const uniqueCategories = extractUniqueCategories(spaces);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/categories/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ export type SpaceOrder = (typeof SPACE_ORDERS)[number];
/**
* @todo fix duplication. Origin at `packages/storage-postgres/src/schema/flags.ts`
*/
export const SPACE_FLAGS = ['sandbox', 'demo'] as const;
export const SPACE_FLAGS = ['sandbox', 'demo', 'archived'] as const;
export type SpaceFlags = (typeof SPACE_FLAGS)[number];
1 change: 1 addition & 0 deletions packages/core/src/space/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './client';
export * from './types';
export * from './utils';
export * from './validation';
11 changes: 10 additions & 1 deletion packages/core/src/space/server/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ type FindAllSpacesProps = {
search?: string;
parentOnly?: boolean;
omitSandbox?: boolean;
omitArchived?: boolean;
};

export const findAllSpaces = async (
{ db }: DbConfig,
{ search, parentOnly = true, omitSandbox = false }: FindAllSpacesProps,
{
search,
parentOnly = true,
omitSandbox = false,
omitArchived = false,
}: FindAllSpacesProps,
) => {
const results = await db
.select({
Expand All @@ -50,6 +56,9 @@ export const findAllSpaces = async (
omitSandbox
? not(sql`${spaces.flags} @> '["sandbox"]'::jsonb`)
: undefined,
omitArchived
? not(sql`${spaces.flags} @> '["archived"]'::jsonb`)
: undefined,
search
? sql`(
-- Full-text search for exact word matches (highest priority)
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/space/server/web3/get-all-spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { db } from '@hypha-platform/storage-postgres';
import { findAllSpaces } from '@hypha-platform/core/server';
import { Space } from '@hypha-platform/core/client';
import { Space, isSpaceArchived } from '@hypha-platform/core/client';
import {
fetchSpaceDetails,
fetchSpaceProposalsIds,
Expand All @@ -13,6 +13,7 @@ interface GetAllSpacesProps {
search?: string;
parentOnly?: boolean;
omitSandbox?: boolean;
omitArchived?: boolean;
}

export async function getAllSpaces(
Expand All @@ -35,9 +36,10 @@ export async function getAllSpaces(
const details = formMap(web3details);
const proposalsIds = formMap(web3proposalsIds);

return spaces.map((space) => {
const enrichedSpaces = spaces.map((space) => {
if (space.web3SpaceId === null) {
return space;
// No web3 data = no on-chain members, treat as archived when filtering
return { ...space, memberCount: 0 };
}

const spaceDetails = details.get(BigInt(space.web3SpaceId));
Expand All @@ -54,6 +56,12 @@ export async function getAllSpaces(
documentCount: spaceProposals?.accepted.length ?? 0,
};
});

if (props.omitArchived) {
return enrichedSpaces.filter((space) => !isSpaceArchived(space));
}

return enrichedSpaces;
} catch (error) {
throw new Error('Failed to get spaces', {
cause: error instanceof Error ? error : new Error(String(error)),
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/space/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Determines if a space should be considered archived.
* A space is archived if:
* - It has the 'archived' flag, OR
* - It has 0 members (memberCount is explicitly 0 from Web3 data)
*/
export function isSpaceArchived(space: {
flags?: string[];
memberCount?: number;
}): boolean {
return (
space.flags?.includes('archived') === true ||
(space.memberCount !== undefined && space.memberCount === 0)
);
}
64 changes: 58 additions & 6 deletions packages/epics/src/spaces/components/create-space-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,16 +300,23 @@ export const SpaceForm = ({
[flags],
);
const isDemo = React.useMemo(() => flags?.includes('demo') ?? false, [flags]);
const isArchived = React.useMemo(
() => flags?.includes('archived') ?? false,
[flags],
);
const isLive = React.useMemo(
() => !isDemo && !isSandbox,
[isDemo, isSandbox],
() => !isDemo && !isSandbox && !isArchived,
[isDemo, isSandbox, isArchived],
);

const toggleSandbox = React.useCallback(() => {
const current = form.getValues().flags ?? [];
const next = current.includes('sandbox')
? current.filter((f) => f !== 'sandbox')
: (['sandbox', ...current.filter((f) => f !== 'demo')] as SpaceFlags[]);
: ([
'sandbox',
...current.filter((f) => f !== 'demo' && f !== 'archived'),
] as SpaceFlags[]);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
if (next.includes('sandbox')) {
form.clearErrors('categories');
Expand All @@ -320,13 +327,29 @@ export const SpaceForm = ({
const current = form.getValues().flags ?? [];
const next = current.includes('demo')
? current.filter((f) => f !== 'demo')
: (['demo', ...current.filter((f) => f !== 'sandbox')] as SpaceFlags[]);
: ([
'demo',
...current.filter((f) => f !== 'sandbox' && f !== 'archived'),
] as SpaceFlags[]);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
}, [form]);

const toggleArchived = React.useCallback(() => {
const current = form.getValues().flags ?? [];
const next = current.includes('archived')
? current.filter((f) => f !== 'archived')
: ([
'archived',
...current.filter((f) => f !== 'demo' && f !== 'sandbox'),
] as SpaceFlags[]);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
}, [form]);

const toggleLive = React.useCallback(() => {
const current = form.getValues().flags ?? [];
const next = current.filter((f) => f !== 'demo' && f !== 'sandbox');
const next = current.filter(
(f) => f !== 'demo' && f !== 'sandbox' && f !== 'archived',
);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
}, [form]);

Expand Down Expand Up @@ -363,6 +386,7 @@ export const SpaceForm = ({
async (space) => {
if (
!space.flags?.includes('sandbox') &&
!space.flags?.includes('archived') &&
space.categories.length === 0
) {
showCategoriesError();
Expand All @@ -377,7 +401,11 @@ export const SpaceForm = ({
(e) => {
const flags = form.getValues()['flags'];
const categories = form.getValues()['categories'];
if (!flags?.includes('sandbox') && categories.length === 0) {
if (
!flags?.includes('sandbox') &&
!flags?.includes('archived') &&
categories.length === 0
) {
showCategoriesError();
}
if (parentSpaceId === -1) {
Expand Down Expand Up @@ -678,6 +706,30 @@ export const SpaceForm = ({
</span>
</div>
</Card>
{label === 'configure' && (
<Card
className={clsx(
'flex p-6 cursor-pointer space-x-4 items-center',
{
'border-accent-9': isArchived,
'hover:border-accent-5': !isArchived,
},
)}
onClick={toggleArchived}
>
<div className="flex flex-col">
<span className="text-2 font-medium">Archive Mode</span>
<span className="text-1 text-neutral-11">
<span>
Archive this space to temporarily pause activity or
deactivate it while keeping all data and history safe. You
can reactivate it anytime by selecting a different
activation mode.
</span>
</span>
</div>
</Card>
)}
</div>
<div className="flex justify-end w-full">
<Button
Expand Down
18 changes: 14 additions & 4 deletions packages/epics/src/spaces/components/explore-spaces.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
'use client';

import { Category, Space, SpaceOrder } from '@hypha-platform/core/client';
import {
Category,
Space,
SpaceOrder,
isSpaceArchived,
} from '@hypha-platform/core/client';
import { SpaceCardList, SpaceSearch } from '@hypha-platform/epics';
import { Locale } from '@hypha-platform/i18n';
import { Text } from '@radix-ui/themes';
Expand Down Expand Up @@ -111,14 +116,19 @@ export function ExploreSpaces({
const searchParams = useSearchParams();
const { replace } = useRouter();

const nonArchivedSpaces = React.useMemo(
() => spaces.filter((space) => !isSpaceArchived(space)),
[spaces],
);

const categoryFilteredSpaces = React.useMemo(
() =>
categories && categories.length > 0
? spaces.filter((space) =>
? nonArchivedSpaces.filter((space) =>
categoriesIntersected(space.categories, categories),
)
: spaces,
[spaces, categories],
: nonArchivedSpaces,
[nonArchivedSpaces, categories],
);

const {
Expand Down
27 changes: 22 additions & 5 deletions packages/epics/src/spaces/components/my-filtered-spaces.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client';

import { Locale } from '@hypha-platform/i18n';
import { Address, Space } from '@hypha-platform/core/client';
import { Address, Space, isSpaceArchived } from '@hypha-platform/core/client';
import { SpaceCardList, useMemberWeb3SpaceIds } from '@hypha-platform/epics';
import { useMe } from '@hypha-platform/core/client';
import React from 'react';
import { Text } from '@radix-ui/themes';
import { useFilterSpacesListWithDiscoverability } from '../hooks/use-spaces-discoverability-batch';
import { SectionFilter, Input } from '@hypha-platform/ui';

export function filterSpaces(
spaces: Space[],
Expand Down Expand Up @@ -36,6 +37,7 @@ export function MyFilteredSpaces({
const { web3SpaceIds } = useMemberWeb3SpaceIds({
personAddress: person?.address as Address | undefined,
});
const [hideArchivedSpaces, setHideArchivedSpaces] = React.useState(true);

const memberFilteredSpaces = React.useMemo(
() => filterSpaces(spaces, person?.slug, web3SpaceIds),
Expand All @@ -48,14 +50,29 @@ export function MyFilteredSpaces({
useGeneralState: false,
});

const displayedSpaces = React.useMemo(() => {
if (hideArchivedSpaces) {
return filteredSpaces.filter((space) => !isSpaceArchived(space));
}
return filteredSpaces;
}, [filteredSpaces, hideArchivedSpaces]);

return (
<div className="space-y-6">
<div className="justify-between items-center flex">
<Text className="text-4">My Spaces | {filteredSpaces.length}</Text>
</div>
<SectionFilter count={displayedSpaces.length} label="My Spaces">
<label className="flex items-center gap-1">
<Input
type="checkbox"
checked={hideArchivedSpaces}
onChange={(e) => setHideArchivedSpaces(e.target.checked)}
className="h-4 w-4"
/>
<span>Hide archived spaces</span>
</label>
</SectionFilter>
<SpaceCardList
lang={lang}
spaces={filteredSpaces}
spaces={displayedSpaces}
showLoadMore={showLoadMore}
showExitButton={true}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { Space } from '@hypha-platform/core/client';
import { Space, isSpaceArchived } from '@hypha-platform/core/client';
import { SpaceCard } from './space-card';
import {
DEFAULT_SPACE_AVATAR_IMAGE,
Expand Down Expand Up @@ -40,6 +40,7 @@ export function SpaceCardWithDiscoverability({
agreements={space.documentCount}
isSandbox={space.flags?.includes('sandbox') ?? false}
isDemo={space.flags?.includes('demo') ?? false}
isArchived={isSpaceArchived(space)}
web3SpaceId={space.web3SpaceId as number}
configPath={`${getHref(space.slug).replace(
/\/+$/,
Expand Down
Loading
Loading