Repository navigation
feat(mobile): archive cloud workspaces, and a Cloud scope that matches desktop's sidebar #8071
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { CreatorFilterScreen } from "@/screens/(authenticated)/(home)/filter/creator"; | ||
|
|
||
| export default CreatorFilterScreen; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { StatusFilterScreen } from "@/screens/(authenticated)/(home)/filter/status"; | ||
|
|
||
| export default StatusFilterScreen; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| import { useLocalSearchParams } from "expo-router"; | ||
| import { WorkspaceScreen } from "@/screens/(authenticated)/workspace/[id]/WorkspaceScreen"; | ||
|
|
||
| export default WorkspaceScreen; | ||
| export default function WorkspaceRoute() { | ||
| const { id } = useLocalSearchParams<{ id: string }>(); | ||
| return <WorkspaceScreen key={id} />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { | ||
| getArchivedCloudWorkspacesQueryKey, | ||
| useArchivedCloudWorkspaces, | ||
| } from "./useArchivedCloudWorkspaces"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { FEATURE_FLAGS } from "@superset/shared/constants"; | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { TRPCClientError } from "@trpc/client"; | ||
| import { useFeatureFlag } from "posthog-react-native"; | ||
| import { useSession } from "@/lib/auth/client"; | ||
| import { apiClient } from "@/lib/trpc/client"; | ||
| import { withPendingCloudMoves } from "../useCloudWorkspaceActions/pendingCloudMoves"; | ||
| import type { CloudWorkspaceRow } from "../useCloudWorkspaces"; | ||
| import { reviveCloudWorkspaceRows } from "../useCloudWorkspaces/reviveCloudWorkspaceRow"; | ||
|
|
||
| const NO_ROWS: CloudWorkspaceRow[] = []; | ||
|
|
||
| export function getArchivedCloudWorkspacesQueryKey( | ||
| organizationId: string | null, | ||
| ) { | ||
| return [ | ||
| "cloud", | ||
| "cloudWorkspace", | ||
| "list", | ||
| organizationId, | ||
| "archived", | ||
| ] as const; | ||
| } | ||
|
|
||
| export function useArchivedCloudWorkspaces({ | ||
| enabled = true, | ||
| }: { | ||
| enabled?: boolean; | ||
| } = {}): { | ||
| workspaces: CloudWorkspaceRow[]; | ||
| isReady: boolean; | ||
| } { | ||
| const enabledByFlag = Boolean(useFeatureFlag(FEATURE_FLAGS.CLOUD_WORKSPACES)); | ||
| const { data: session } = useSession(); | ||
| const organizationId = session?.session?.activeOrganizationId ?? null; | ||
|
|
||
| const query = useQuery({ | ||
| queryKey: getArchivedCloudWorkspacesQueryKey(organizationId), | ||
| enabled: enabled && enabledByFlag && organizationId !== null, | ||
| networkMode: "always" as const, | ||
| retry: (count, error) => | ||
| !(error instanceof TRPCClientError && error.data?.code === "FORBIDDEN") && | ||
| count < 2, | ||
| select: reviveCloudWorkspaceRows, | ||
| queryFn: async (): Promise<CloudWorkspaceRow[]> => { | ||
| if (!organizationId) return NO_ROWS; | ||
| try { | ||
| return withPendingCloudMoves( | ||
| "archived", | ||
| await apiClient.cloudWorkspace.list.query({ | ||
| organizationId, | ||
| archived: true, | ||
| }), | ||
| ); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof TRPCClientError && | ||
| error.data?.code === "FORBIDDEN" | ||
| ) { | ||
| return NO_ROWS; | ||
| } | ||
| throw error; | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| return { | ||
| workspaces: query.data ?? NO_ROWS, | ||
| isReady: | ||
| !enabled || | ||
| !enabledByFlag || | ||
| organizationId === null || | ||
| query.isSuccess || | ||
| query.isError, | ||
| }; | ||
|
Comment on lines
+67
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the archived-list request fails after its retries, this hook marks the result ready and supplies an empty array. Home then says “No archived workspaces” instead of indicating that it could not check, which can conceal existing archived workspaces during an API failure. Knowledge Base Used: Mobile product experience Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts
Line: 67-75
Comment:
**Fetch failures look empty**
When the archived-list request fails after its retries, this hook marks the result ready and supplies an empty array. Home then says “No archived workspaces” instead of indicating that it could not check, which can conceal existing archived workspaces during an API failure.
**Knowledge Base Used:** [Mobile product experience](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/mobile-product-experience.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
|
Comment on lines
+67
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '62,78p' apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts
sed -n '236,255p' 'apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx'
sed -n '785,816p' 'apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx'
sed -n '210,248p' 'apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx'Repository: superset-sh/superset Length of output: 3222 🏁 Script executed: set -eu
printf '%s\n' '--- hook ---'
cat -n apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts | sed -n '1,115p'
printf '%s\n' '--- Home archived data and refresh ---'
rg -n -C 5 'useArchivedCloudWorkspaces|archivedCloud|archivedReady|onRefresh|refetch' 'apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx'
printf '%s\n' '--- Search archived data and states ---'
cat -n 'apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx' | sed -n '1,280p'
printf '%s\n' '--- Status filter ---'
cat -n 'apps/mobile/screens/(authenticated)/(home)/filter/status/StatusFilterScreen.tsx' | sed -n '1,120p'
printf '%s\n' '--- introduced diff summary ---'
git diff --stat 145a162be04f76163aee0b561e18a252b6e86562 1ce078f6b842bf8ad0f2176e1237bc5a9662b87d -- apps/mobile/hooks/useArchivedCloudWorkspaces apps/mobile/screensRepository: superset-sh/superset Length of output: 23322 🏁 Script executed: set -eu
cat -n apps/mobile/hooks/useArchivedCloudWorkspaces/useArchivedCloudWorkspaces.ts | sed -n '1,115p'
rg -n -C 5 'useArchivedCloudWorkspaces|archivedCloud|archivedReady|onRefresh|refetch' 'apps/mobile/screens/(authenticated)/(home)/home/HomeScreen.tsx'
cat -n 'apps/mobile/screens/(authenticated)/(home)/search/SearchScreen.tsx' | sed -n '1,280p'
cat -n 'apps/mobile/screens/(authenticated)/(home)/filter/status/StatusFilterScreen.tsx' | sed -n '1,120p'
git diff --stat 145a162be04f76163aee0b561e18a252b6e86562 1ce078f6b842bf8ad0f2176e1237bc5a9662b87d -- apps/mobile/hooks/useArchivedCloudWorkspaces apps/mobile/screensRepository: superset-sh/superset Length of output: 23176 Keep archived-query failures separate from an empty archive. When The query retries twice, and Home refresh invalidates the Suggested fix }: {
workspaces: CloudWorkspaceRow[];
isReady: boolean;
+ isError: boolean;
} {
@@
return {
workspaces: query.data ?? NO_ROWS,
+ isError: query.isError,
isReady:🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; | ||
| import { | ||
| beginCloudMove, | ||
| endCloudMove, | ||
| withPendingCloudMoves, | ||
| } from "./pendingCloudMoves"; | ||
|
|
||
| const row = (id: string, status: CloudWorkspaceRow["status"]) => | ||
| ({ id, status }) as CloudWorkspaceRow; | ||
|
|
||
| describe("withPendingCloudMoves", () => { | ||
| test("keeps a row in the list it was just moved to until the move ends", () => { | ||
| const archiving = row("a", "deleted"); | ||
| beginCloudMove("archived", archiving); | ||
|
|
||
| const active = withPendingCloudMoves("active", [ | ||
| row("a", "ready"), | ||
| row("b", "ready"), | ||
| ]); | ||
| const archived = withPendingCloudMoves("archived", []); | ||
| expect(active.map((r) => r.id)).toEqual(["b"]); | ||
| expect(archived).toEqual([archiving]); | ||
|
|
||
| endCloudMove("a"); | ||
| expect( | ||
| withPendingCloudMoves("active", [row("a", "ready")]).map((r) => r.id), | ||
| ).toEqual(["a"]); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { CloudWorkspaceRow } from "@/hooks/useCloudWorkspaces"; | ||
|
|
||
| type List = "active" | "archived"; | ||
|
|
||
| const pending = new Map<string, { to: List; row: CloudWorkspaceRow }>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The Prompt for AI agents |
||
|
|
||
| export function beginCloudMove(to: List, row: CloudWorkspaceRow): void { | ||
| pending.set(row.id, { to, row }); | ||
| } | ||
|
|
||
| export function endCloudMove(id: string): void { | ||
| pending.delete(id); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: An earlier archive/unarchive request can finish while a newer move for the same ID is still pending, and this deletion clears the newer move's protection. Track a per-move token and only clear the entry created by the completing request. Prompt for AI agents |
||
| } | ||
|
|
||
| /** | ||
| * A list fetched while an archive or unarchive is in flight still has the row | ||
| * where it was; keep it where the user just moved it until the request settles. | ||
| */ | ||
| export function withPendingCloudMoves( | ||
| list: List, | ||
| rows: CloudWorkspaceRow[], | ||
| ): CloudWorkspaceRow[] { | ||
| if (pending.size === 0) return rows; | ||
| const kept = rows.filter((row) => { | ||
| const move = pending.get(row.id); | ||
| return !move || move.to === list; | ||
| }); | ||
| for (const [id, move] of pending) { | ||
| if (move.to === list && !kept.some((row) => row.id === id)) { | ||
| kept.unshift(move.row); | ||
|
Comment on lines
+28
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If someone switches organizations while an archive or unarchive request is in flight, this loop can add the previous organization’s workspace to the new organization’s list. Home renders the row without checking its organization, exposing its name and metadata until the request settles. Scope pending moves to the organization that started them. How this was verified: Organization-scoped list queries apply a shared pending map that inserts rows without checking their organization. Knowledge Base Used: Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/mobile/hooks/useCloudWorkspaceActions/pendingCloudMoves.ts
Line: 28-30
Comment:
**Pending rows cross organizations**
If someone switches organizations while an archive or unarchive request is in flight, this loop can add the previous organization’s workspace to the new organization’s list. Home renders the row without checking its organization, exposing its name and metadata until the request settles. Scope pending moves to the organization that started them.
**How this was verified:** Organization-scoped list queries apply a shared pending map that inserts rows without checking their organization.
**Knowledge Base Used:**
- [Mobile product experience](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/mobile-product-experience.md)
- [API, SDK, and RPC contracts](https://app.greptile.com/personal-org-6810/-/custom-context/knowledge-base/superset-sh/superset/-/docs/api-sdk-and-rpc-contracts.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
| } | ||
| return kept; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: A failed archived-list request is treated as an answered empty list, so Home displays “No archived workspaces” and hides the failure. Expose the query error and render a failure state instead of folding
query.isErrorinto readiness.Prompt for AI agents