Repository: giselles-ai/giselle
Pull request #516: Develop /settings/account
Author: login: okky-hi
Created: 2025-03-24T08:58:16Z | Merged: 2025-03-25T06:47:42Z

## Summary
<!-- Briefly describe the changes and the purpose of the PR. -->
Edited and added related components and files to reproduce the account page design.


## Related Issue
<!-- Mention the related issue number if applicable. -->
https://github.com/giselles-ai/giselle/issues/413

## Changes
<!-- List the main changes made in this PR. -->
- Create a new page
  - `apps/studio.giselles.ai/app/(main)/settings/account/v2/page.tsx`
    - Display `apps/studio.giselles.ai/app/(main)/settings/account/page.tsx` if the SETTING_V2 flag is not enabled
- Create new components to be used exclusively in settings V2
- Fix fetchUserTeams to return the role.

## Testing
<!-- Briefly describe the testing steps or results. -->
Open [Preview and /settings/account](https://giselle-git-fork-okky-hi-feature-settings-account-r06-edge.vercel.app/settings/account) , on/off settings-v2 flag

## Other Information
<!-- Add any other relevant information for the reviewer. -->



Diff:
diff --git a/apps/studio.giselles.ai/app/(main)/settings/account/page.tsx b/apps/studio.giselles.ai/app/(main)/settings/account/page.tsx
index cff47ce13d..bd9e900ff2 100644
--- a/apps/studio.giselles.ai/app/(main)/settings/account/page.tsx
+++ b/apps/studio.giselles.ai/app/(main)/settings/account/page.tsx
@@ -2,6 +2,7 @@ import { ClickableText } from "@/components/ui/clicable-text";
 import { Field } from "@/components/ui/field";
 import { Label } from "@/components/ui/label";
 import { Skeleton } from "@/components/ui/skeleton";
+import { settingsV2Flag } from "@/flags";
 import { ToastProvider } from "@/packages/contexts/toast";
 import Link from "next/link";
 import { Suspense } from "react";
@@ -11,8 +12,13 @@ import { AccountToasts } from "./account-toasts";
 import { getAccountInfo } from "./actions";
 import { GitHubAuthentication } from "./github-authentication";
 import { GoogleAuthentication } from "./google-authentication";
+import AccountSettingPageV2 from "./v2/page";
 
 export default async function AccountSettingPage() {
+	const settingsV2Mode = await settingsV2Flag();
+	if (settingsV2Mode) {
+		return <AccountSettingPageV2 />;
+	}
 	const { displayName, email } = await getAccountInfo();
 
 	return (
diff --git a/apps/studio.giselles.ai/app/(main)/settings/account/v2/page.tsx b/apps/studio.giselles.ai/app/(main)/settings/account/v2/page.tsx
new file mode 100644
index 0000000000..d157e52813
--- /dev/null
+++ b/apps/studio.giselles.ai/app/(main)/settings/account/v2/page.tsx
@@ -0,0 +1,42 @@
+import { Button } from "@/components/v2/ui/button";
+import { ToastProvider } from "@/packages/contexts/toast";
+import { fetchUserTeams } from "@/services/teams";
+import TeamCreation from "@/services/teams/components/v2/team-creation";
+import { Card } from "../../components/v2/card";
+import { AccountToasts } from "../account-toasts";
+import UserTeams from "./user-teams";
+
+export default async function AccountSettingPageV2() {
+	const teams = await fetchUserTeams();
+
+	return (
+		<ToastProvider>
+			<div className="flex flex-col gap-[24px]">
+				<h3
+					className="text-primary-100 font-semibold text-[28px] leading-[28px] tracking-[-0.011em] font-hubot"
+					style={{ textShadow: "0px 0px 20px hsla(207, 100%, 48%, 1)" }}
+				>
+					Overview
+				</h3>
+				<div className="flex flex-col gap-y-[16px]">
+					<Card
+						title="Team"
+						description="The teams that are associated with your Giselle’s account."
+						action={{
+							component: (
+								<div className="grid placeitems-center">
+									<TeamCreation>
+										<Button>Create new team</Button>
+									</TeamCreation>
+								</div>
+							),
+						}}
+					>
+						<UserTeams teams={teams} />
+					</Card>
+				</div>
+			</div>
+			<AccountToasts />
+		</ToastProvider>
+	);
+}
diff --git a/apps/studio.giselles.ai/app/(main)/settings/account/v2/user-teams.tsx b/apps/studio.giselles.ai/app/(main)/settings/account/v2/user-teams.tsx
new file mode 100644
index 0000000000..0bc5b698fe
--- /dev/null
+++ b/apps/studio.giselles.ai/app/(main)/settings/account/v2/user-teams.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { Search } from "lucide-react";
+import { useState } from "react";
+
+const roles = {
+	admin: "Admin",
+	member: "Member",
+};
+
+export default function UserTeams({
+	teams,
+}: { teams: { id: string; name: string; role: "admin" | "member" }[] }) {
+	const [teamName, setTeamName] = useState("");
+
+	const handleChangeTeamName = (e: React.ChangeEvent<HTMLInputElement>) => {
+		setTeamName(e.target.value);
+	};
+
+	const filteredTeams = teams.filter((team) =>
+		team.name.toLowerCase().includes(teamName.toLowerCase()),
+	);
+
+	return (
+		<>
+			<div className="flex items-center gap-x-[11px] py-2 px-3 border-[0.5px] border-black-820/50 rounded-[8px] bg-black-350/20">
+				<Search className="size-6 text-black-400" />
+				<input
+					onChange={handleChangeTeamName}
+					type="text"
+					defaultValue={teamName}
+					placeholder="Search for a team..."
+					className="w-full text-white-900 font-medium text-[14px] leading-[23.8px] font-geist placeholder:text-black-400"
+				/>
+			</div>
+			<div className="border-[0.5px] border-black-400 rounded-[8px] divide-y divide-black-400">
+				{filteredTeams.map((team) => (
+					<UserTeamsItem
+						key={team.id}
+						teamName={team.name}
+						role={roles[team.role]}
+					/>
+				))}
+			</div>
+		</>
+	);
+}
+
+function UserTeamsItem({ teamName, role }: { teamName: string; role: string }) {
+	return (
+		<div className="flex items-center justify-between gap-4 p-4 bg-black-400/10">
+			<div className="flex flex-col">
+				<div className="text-white-400 font-medium text-[16px] leading-[22.4px] font-geist">
+					{teamName}
+				</div>
+				<div className="text-black-400 font-medium text-[12px] leading-[20.4px] font-geist">
+					{role}
+				</div>
+			</div>
+		</div>
+	);
+}
diff --git a/apps/studio.giselles.ai/services/teams/components/v2/team-creation-form.tsx b/apps/studio.giselles.ai/services/teams/components/v2/team-creation-form.tsx
index fa235c1f64..5972a31d1a 100644
--- a/apps/studio.giselles.ai/services/teams/components/v2/team-creation-form.tsx
+++ b/apps/studio.giselles.ai/services/teams/components/v2/team-creation-form.tsx
@@ -44,11 +44,13 @@ function Submit({
 interface TeamCreationFormProps {
 	canCreateFreeTeam: boolean;
 	proPlanPrice: string;
+	children?: React.ReactNode;
 }
 
 export function TeamCreationForm({
 	canCreateFreeTeam,
 	proPlanPrice,
+	children,
 }: TeamCreationFormProps) {
 	const [teamName, setTeamName] = useState("");
 	const [selectedPlan, setSelectedPlan] = useState("");
@@ -56,7 +58,9 @@ export function TeamCreationForm({
 	return (
 		<Dialog>
 			<DialogTrigger asChild className="cursor-pointer">
-				<UserPlus className="h-6 w-6 text-white hover:opacity-80" />
+				{children ?? (
+					<UserPlus className="h-6 w-6 text-white hover:opacity-80" />
+				)}
 			</DialogTrigger>
 			<DialogContent className="sm:max-w-[500px] gap-y-6 px-8 py-6 border-[0.5px] border-black-400 rounded-[8px] bg-black-900">
 				<DialogHeader>
diff --git a/apps/studio.giselles.ai/services/teams/components/v2/team-creation.tsx b/apps/studio.giselles.ai/services/teams/components/v2/team-creation.tsx
index fd991cbc05..2285edac92 100644
--- a/apps/studio.giselles.ai/services/teams/components/v2/team-creation.tsx
+++ b/apps/studio.giselles.ai/services/teams/components/v2/team-creation.tsx
@@ -5,7 +5,9 @@ import invariant from "tiny-invariant";
 import { fetchUserTeams } from "../../fetch-user-teams";
 import { TeamCreationForm } from "./team-creation-form";
 
-export default async function TeamCreation() {
+export default async function TeamCreation({
+	children,
+}: { children?: React.ReactNode }) {
 	const user = await getUser();
 	if (!user) {
 		throw new Error("User not found");
@@ -24,6 +26,8 @@ export default async function TeamCreation() {
 		<TeamCreationForm
 			canCreateFreeTeam={!isInternalUser && !hasExistingFreeTeam}
 			proPlanPrice={proPlanPrice}
-		/>
+		>
+			{children}
+		</TeamCreationForm>
 	);
 }
diff --git a/apps/studio.giselles.ai/services/teams/fetch-user-teams.ts b/apps/studio.giselles.ai/services/teams/fetch-user-teams.ts
index 763b4a7b85..44fcbf1a36 100644
--- a/apps/studio.giselles.ai/services/teams/fetch-user-teams.ts
+++ b/apps/studio.giselles.ai/services/teams/fetch-user-teams.ts
@@ -20,6 +20,7 @@ export async function fetchUserTeams() {
 			name: teams.name,
 			type: teams.type,
 			activeSubscriptionId: subscriptions.id,
+			role: teamMemberships.role,
 		})
 		.from(teams)
 		.innerJoin(teamMemberships, eq(teams.dbId, teamMemberships.teamDbId))


Reviews:
user: login: okky-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:10:09Z
user: login: okky-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:11:28Z
user: login: okky-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:20:34Z
user: login: okky-hi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T04:24:52Z
user: login: satococoa
body: Thank you! 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T06:35:44Z
user: login: shige
body: Thank you! 🚀 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T06:47:36Z

Review comments:
diff_hunk: @@ -0,0 +1,62 @@
+"use client";
+
+import { Search } from "lucide-react";
+import { useState } from "react";
+
+const roles = {
+	admin: "Admin",
+	member: "Member",
+};
+
+export default function UserTeams({
+	teams,
+}: { teams: { id: string; name: string; role: "admin" | "member" }[] }) {
+	const [teamName, setTeamName] = useState("");
+
+	const handleChangeTeamName = (e: React.ChangeEvent<HTMLInputElement>) => {
+		setTeamName(e.target.value);
+	};
+
+	const filteredTeams = teams.filter((team) =>
+		team.name.toLowerCase().includes(teamName.toLowerCase()),
+	);
+
+	return (
+		<>
+			<div className="flex items-center gap-x-[11px] py-2 px-3 border-[0.5px] border-black-820/50 rounded-[8px] bg-black-350/20">
+				<Search className="size-6 text-black-400" />
+				<input
+					onChange={handleChangeTeamName}
+					type="text"
+					defaultValue={teamName}
+					placeholder="Search for a team..."
+					className="w-full text-white-900 font-medium text-[14px] leading-[23.8px] font-geist placeholder:text-black-400"
+				/>
+			</div>
+			<div className="border-[0.5px] border-black-400 rounded-[8px] divide-y divide-black-400">
+				{filteredTeams.map((team) => (
+					<UserTeamsItem
+						key={team.id}
+						teamName={team.name}
+						role={roles[team.role]}
+					/>
+				))}
+			</div>
path: apps/studio.giselles.ai/app/(main)/settings/account/v2/user-teams.tsx
user: login: okky-hi
body: 【About Team Name Search】
Would it be acceptable if nothing is shown when there is no team that matches the entered text?

↓normal time
![名称未設定11](https://github.com/user-attachments/assets/bcade671-b20d-4733-8414-b5835f1d4491)

↓When no team matches the entered text
![名称未設定10](https://github.com/user-attachments/assets/d5db38b4-dbb7-4bc8-a7c6-cab3845323df)

created_at: 2025-03-25T04:10:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,7 @@ export async function fetchUserTeams() {
 			name: teams.name,
 			type: teams.type,
 			activeSubscriptionId: subscriptions.id,
+			role: teamMemberships.role,
path: apps/studio.giselles.ai/services/teams/fetch-user-teams.ts
user: login: okky-hi
body: Fix fetchUserTeams to return the role
created_at: 2025-03-25T04:11:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+"use client";
+
+import { Search } from "lucide-react";
+import { useState } from "react";
+
+const roles = {
+	admin: "Admin",
+	member: "Member",
+};
+
+export default function UserTeams({
+	teams,
+}: { teams: { id: string; name: string; role: "admin" | "member" }[] }) {
+	const [teamName, setTeamName] = useState("");
+
+	const handleChangeTeamName = (e: React.ChangeEvent<HTMLInputElement>) => {
+		setTeamName(e.target.value);
+	};
+
+	const filteredTeams = teams.filter((team) =>
+		team.name.toLowerCase().includes(teamName.toLowerCase()),
+	);
+
+	return (
+		<>
+			<div className="flex items-center gap-x-[11px] py-2 px-3 border-[0.5px] border-black-820/50 rounded-[8px] bg-black-350/20">
+				<Search className="size-6 text-black-400" />
+				<input
+					onChange={handleChangeTeamName}
+					type="text"
+					defaultValue={teamName}
+					placeholder="Search for a team..."
+					className="w-full text-white-900 font-medium text-[14px] leading-[23.8px] font-geist placeholder:text-black-400"
+				/>
+			</div>
+			<div className="border-[0.5px] border-black-400 rounded-[8px] divide-y divide-black-400">
+				{filteredTeams.map((team) => (
+					<UserTeamsItem
+						key={team.id}
+						teamName={team.name}
+						role={roles[team.role]}
path: apps/studio.giselles.ai/app/(main)/settings/account/v2/user-teams.tsx
user: login: okky-hi
body: Displays the string corresponding to the retrieved role.

![名称未設定11のコピー](https://github.com/user-attachments/assets/4e5e7ec4-666b-4740-ac40-e51f8de48886)

created_at: 2025-03-25T04:20:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,19 +44,23 @@ function Submit({
 interface TeamCreationFormProps {
 	canCreateFreeTeam: boolean;
 	proPlanPrice: string;
+	children?: React.ReactNode;
 }
 
 export function TeamCreationForm({
 	canCreateFreeTeam,
 	proPlanPrice,
+	children,
 }: TeamCreationFormProps) {
 	const [teamName, setTeamName] = useState("");
 	const [selectedPlan, setSelectedPlan] = useState("");
 
 	return (
 		<Dialog>
 			<DialogTrigger asChild className="cursor-pointer">
-				<UserPlus className="h-6 w-6 text-white hover:opacity-80" />
+				{children ?? (
+					<UserPlus className="h-6 w-6 text-white hover:opacity-80" />
+				)}
path: apps/studio.giselles.ai/services/teams/components/v2/team-creation-form.tsx
user: login: okky-hi
body: The `UserPlus` icon is shown by default when no children are provided, but you can override it with custom content if needed

![名称未設定12](https://github.com/user-attachments/assets/81370fab-9e72-4244-8d45-22a4e9b5db89)

created_at: 2025-03-25T04:24:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-24T08:58:20Z
body: ###  ⚠️  No Changeset found

Latest commit: 5499ef7dee968034d8f02215ee2cd41cb3df93f0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**



[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/okky-hi/giselle/new/feature-settings-account?filename=.changeset/kind-terms-tan.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0ADevelop%20%2Fsettings%2Faccount%0A)

<details><summary>💥 An error occurred when fetching the changed packages and changesets in this PR</summary>

```
Some errors occurred when validating the changesets config:
The package "playground" depends on the ignored package "giselle-sdk", but "playground" is not being ignored. Please add "playground" to the `ignore` option.
The package "studio.giselles.ai" depends on the ignored package "giselle-sdk", but "studio.giselles.ai" is not being ignored. Please add "studio.giselles.ai" to the `ignore` option.
The package "@giselle-internal/workflow-designer-ui" depends on the ignored package "giselle-sdk", but "@giselle-internal/workflow-designer-ui" is not being ignored. Please add "@giselle-internal/workflow-designer-ui" to the `ignore` option.
```

</details>

author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-24T08:58:20Z
body: @okky-hi is attempting to deploy a commit to the **Edge** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Edge&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22948b477cf0da5ee1bd25693dac2a6280cb1e60cd%22%7D%2C%22id%22%3A%22QmQ6KquUhR9vjaDUKRUFQc7mejCGbrk4riKtGgFnT2Fv7z%22%2C%22org%22%3A%22giselles-ai%22%2C%22prId%22%3A516%2C%22repo%22%3A%22giselle%22%7D).


author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-24T11:27:19Z
body: [vc]: #75oHbRqttq3zLMzoH4JzrGa+1PMYpv7anqGKbXlTn3U=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJnaXNlbGxlIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvLmdpc2VsbGVzLmFpIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3IwNi1lZGdlL2dpc2VsbGUvRVR2THB5Z250MWllYmpEY2drZUY5WVNmWWpjMyIsInByZXZpZXdVcmwiOiJnaXNlbGxlLWdpdC1mb3JrLW9ra3ktaGktZmVhdHVyZS1zZXR0aW5ncy1hY2NvdW50LXIwNi1lZGdlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZ2lzZWxsZS1naXQtZm9yay1va2t5LWhpLWZlYXR1cmUtc2V0dGluZ3MtYWNjb3VudC1yMDYtZWRnZS52ZXJjZWwuYXBwIn19LHsibmFtZSI6Imdpc2VsbGUtcGxheWdyb3VuZCIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3BsYXlncm91bmQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcjA2LWVkZ2UvZ2lzZWxsZS1wbGF5Z3JvdW5kLzg0S3JXUUpEWjc3UlZ0d3RueXZpZURBaXdMbjciLCJwcmV2aWV3VXJsIjoiZ2lzZWxsZS1wbGF5Z3JvdW5kLWdpdC1mb3JrLW9ra3ktaGktZmVhdHVyZS1zZXQtOGE0MTk2LXIwNi1lZGdlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **giselle** | ✅ Ready ([Inspect](https://vercel.com/r06-edge/giselle/ETvLpygnt1iebjDcgkeF9YSfYjc3)) | [Visit Preview](https://giselle-git-fork-okky-hi-feature-settings-account-r06-edge.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/giselle-git-fork-okky-hi-feature-settings-account-r06-edge.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 4:03am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **giselle-playground** | ⬜️ Ignored ([Inspect](https://vercel.com/r06-edge/giselle-playground/84KrWQJDZ77RVtwtnyvieDAiwLn7)) | [Visit Preview](https://giselle-playground-git-fork-okky-hi-feature-set-8a4196-r06-edge.vercel.app) |  | Mar 25, 2025 4:03am |
</details>



author_association: NONE