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
4 changes: 3 additions & 1 deletion forms_pro/api/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_team_members(team_id: str) -> list[GetTeamMembersResponse]:


@frappe.whitelist(methods=["POST"])
def create_team(team_name: str) -> FPTeam:
def create_team(team_name: str, logo_url: str | None = None) -> FPTeam:
"""
Create a new team

Expand All @@ -56,6 +56,8 @@ def create_team(team_name: str) -> FPTeam:

team: FPTeam = frappe.new_doc("FP Team")
team.team_name = team_name
if logo_url:
team.logo = logo_url
team.insert()
return team

Expand Down
1 change: 1 addition & 0 deletions forms_pro/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
class GetUserTeamsResponseSchema(BaseModel):
name: str = Field(description="ID of the team")
team_name: str = Field(description="The name of the team")
logo: str | None = Field(description="Logo of the team")
is_current: bool = Field(description="Whether this is the current team")


Expand Down
8 changes: 7 additions & 1 deletion forms_pro/forms_pro/doctype/fp_team/fp_team.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"logo",
"team_name",
"users"
],
Expand All @@ -22,12 +23,17 @@
"fieldtype": "Table MultiSelect",
"label": "Users",
"options": "FP Team Member"
},
{
"fieldname": "logo",
"fieldtype": "Attach Image",
"label": "Logo"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-19 19:43:50.515720",
"modified": "2026-02-12 19:54:29.693501",
"modified_by": "Administrator",
"module": "Forms Pro",
"name": "FP Team",
Expand Down
1 change: 1 addition & 0 deletions forms_pro/forms_pro/doctype/fp_team/fp_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class FPTeam(Document):

from forms_pro.forms_pro.doctype.fp_team_member.fp_team_member import FPTeamMember

logo: DF.AttachImage | None
team_name: DF.Data
users: DF.TableMultiSelect[FPTeamMember]
# end: auto-generated types
Expand Down
Binary file added forms_pro/public/avatars/avatar-1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-10.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-11.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-12.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-13.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-14.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-15.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-16.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-17.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-18.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-19.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-20.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-21.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-22.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-23.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-24.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-25.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-26.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-27.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-28.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-29.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-30.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-31.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added forms_pro/public/avatars/avatar-32.jpg
Binary file added forms_pro/public/avatars/avatar-33.jpg
Binary file added forms_pro/public/avatars/avatar-34.jpg
Binary file added forms_pro/public/avatars/avatar-35.jpg
Binary file added forms_pro/public/avatars/avatar-36.jpg
Binary file added forms_pro/public/avatars/avatar-37.jpg
Binary file added forms_pro/public/avatars/avatar-38.jpg
Binary file added forms_pro/public/avatars/avatar-39.jpg
Binary file added forms_pro/public/avatars/avatar-4.jpg
Binary file added forms_pro/public/avatars/avatar-40.jpg
Binary file added forms_pro/public/avatars/avatar-41.jpg
Binary file added forms_pro/public/avatars/avatar-42.jpg
Binary file added forms_pro/public/avatars/avatar-43.jpg
Binary file added forms_pro/public/avatars/avatar-44.jpg
Binary file added forms_pro/public/avatars/avatar-45.jpg
Binary file added forms_pro/public/avatars/avatar-46.jpg
Binary file added forms_pro/public/avatars/avatar-47.jpg
Binary file added forms_pro/public/avatars/avatar-48.jpg
Binary file added forms_pro/public/avatars/avatar-49.jpg
Binary file added forms_pro/public/avatars/avatar-5.jpg
Binary file added forms_pro/public/avatars/avatar-50.jpg
Binary file added forms_pro/public/avatars/avatar-6.jpg
Binary file added forms_pro/public/avatars/avatar-7.jpg
Binary file added forms_pro/public/avatars/avatar-8.jpg
Binary file added forms_pro/public/avatars/avatar-9.jpg
2 changes: 2 additions & 0 deletions forms_pro/public/avatars/avatar_attribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
These avatars are sourced from [Outspace Studios](https://avatars.outpace.systems/).
The original source is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
1 change: 1 addition & 0 deletions forms_pro/utils/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def get_user_teams(user: str = frappe.session.user):
.where(FP_TEAM_MEMBER.user == user)
.select(
FP_TEAM.team_name,
FP_TEAM.logo,
FP_TEAM.name,
Case().when(FP_TEAM.name == user_default_team, True).else_(False).as_("is_current"),
)
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@vueuse/core": "^13.9.0",
"dayjs": "^1.11.19",
"feather-icons": "^4.29.2",
"frappe-ui": "^0.1.244",
"frappe-ui": "^0.1.262",
"lucide-vue-next": "^0.543.0",
"pinia": "^3.0.3",
"socket.io-client": "^4.7.2",
Expand Down
5 changes: 5 additions & 0 deletions frontend/public/logo/logo.svg
2 changes: 1 addition & 1 deletion frontend/src/components/fields/Attachment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const value = defineModel<string>();

const inPreview = ref(false);

type FileType = {
export type FileType = {
content_hash: string;
creation: string;
docstatus: number;
Expand Down
91 changes: 80 additions & 11 deletions frontend/src/components/team/CreateTeamDialog.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { Dialog, ErrorMessage, FormControl } from "frappe-ui";
import { Dialog, ErrorMessage, FileUploader } from "frappe-ui";
import * as z from "zod";
import { reactive, ref, watch } from "vue";
import { useUser } from "@/stores/user";
import TeamLogo from "@/components/team/TeamLogo.vue";
import type { FileType } from "@/components/fields/Attachment.vue";

const user = useUser();

const model = defineModel<boolean>({
type: Boolean,
required: true,
Expand All @@ -18,12 +19,14 @@ const formSchema = z.object({
.string()
.min(2, "Team name must be at least 2 characters long")
.max(140, "Team name must be less than 140 characters long"),
logo_url: z.string().optional(),
});

type Form = z.infer<typeof formSchema>;

const form = reactive<Form>({
team_name: "",
logo_url: undefined,
});

watch(
Expand All @@ -46,29 +49,95 @@ function createTeam() {
return;
}
formErrors.value = "";
user.createTeam(form.team_name);
user.createTeam(form.team_name, form.logo_url);
model.value = false;
}

function setTeamLogo(file: FileType) {
form.logo_url = file.file_url;
}

function removeTeamLogo() {
form.logo_url = undefined;
}
</script>
<template>
<Dialog
v-model="model"
:options="{
title: 'Create New Team',
icon: 'users',
}"
>
<template #body-content>
<div class="flex flex-col gap-4">
<FormControl
v-model="form.team_name"
label="Team Name"
variant="outline"
<div
class="flex flex-col items-center gap-4 w-full p-4 rounded bg-surface-gray-1 border border-surface-gray-2"
>
<div class="flex flex-col items-center gap-4 my-2">
<div class="relative">
<Button
v-if="form.logo_url"
variant="outline"
size="sm"
class="!text-xs !size-6 absolute -top-2 -right-2 rounded-full"
icon="x"
@click="removeTeamLogo"
/>
<TeamLogo
:team-name="form.team_name"
class="size-12"
:logo-url="form.logo_url ?? null"
/>
</div>
<div class="text-xs text-ink-gray-6 text-center flex flex-col gap-2">
<p>The team logo is automatically generated based on the team name.</p>
<div class="flex gap-2 items-center justify-center">
<p>Don't like it?</p>
<FileUploader
@success="(file: FileType) => setTeamLogo(file)"
:fileTypes="['image/png', 'image/jpeg', 'image/jpg', 'image/gif']"
>
<!-- @vue-ignore -->
<template
#default="{
file,
uploading,
progress,
uploaded,
message,
error,
total,
success,
openFileSelector,
}"
>
<ErrorMessage :message="error" />
<Button
variant="ghost"
size="sm"
class="!text-xs"
@click="openFileSelector"
:loading="uploading"
>
Upload your own
</Button>
</template>
</FileUploader>
</div>
</div>
</div>
<input
type="text"
required
v-model="form.team_name"
class="text-lg text-ink-gray-9 !outline-0 !ring-0 !border-0 bg-inherit text-center"
placeholder="Enter team name"
/>
<ErrorMessage :message="formErrors" />
<Button @click="createTeam" :disabled="formErrors !== ''" variant="subtle">
<Button
@click="createTeam"
class="w-full"
variant="outline"
:disabled="formErrors !== ''"
>
Create Team
</Button>
</div>
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/team/TeamLogo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { computed } from "vue";

type Props = {
teamName: string;
logoUrl: string | null;
classNames?: string;
};

const props = withDefaults(defineProps<Props>(), {
classNames: "size-5 rounded-full object-cover",
});

const AVATAR_BASE = "/assets/forms_pro/avatars/avatar-";

/** Derives a stable 1–50 index from the team name (length + first/last char) so the same name always maps to the same avatar when no logoUrl is set. */
function avatarIndex(name: string): number {
const n = name.length;
if (n === 0) return 1;
const h = n + name.charCodeAt(0) + (n > 1 ? name.charCodeAt(n - 1) : 0);
return (((h % 50) + 50) % 50) + 1;
}

const avatarSrc = computed(() =>
props.logoUrl ? props.logoUrl : `${AVATAR_BASE}${avatarIndex(props.teamName)}.jpg`
);
</script>

<template>
<img :src="avatarSrc" :alt="teamName" :class="classNames" />
</template>
89 changes: 89 additions & 0 deletions frontend/src/components/team/TeamSwitcher.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { useUser } from "@/stores/user";
import { Dropdown } from "frappe-ui";
import { ChevronsUpDown } from "lucide-vue-next";
import { computed, inject, ref } from "vue";
import CreateTeamDialog from "@/components/team/CreateTeamDialog.vue";
import TeamSwitcherItem from "@/components/team/TeamSwitcherItem.vue";
import TeamLogo from "@/components/team/TeamLogo.vue";

const isSidebarCollapsed = inject("isSidebarCollapsed");

const showCreateTeamDialog = ref(false);
const userStore = useUser();

const teamOptions = computed(() => {
return userStore.userTeams
?.filter((team) => team.name !== userStore.currentTeam?.name)
.map((team) => ({
label: `${team.team_name}`,
logoUrl: team.logo ?? undefined,
isTeam: true,
onClick: () => {
userStore.switchTeam(team);
},
}));
});

const groupOptions = computed(() => {
return [
{
group: "Switch Team",
items: teamOptions.value,
},
{
group: "",
items: [
{
label: "Create New Team",
onClick: () => {
showCreateTeamDialog.value = true;
},
icon: "plus",
},
],
},
];
});
</script>
<template>
<CreateTeamDialog v-model="showCreateTeamDialog" />
<Dropdown :options="groupOptions">
<template #default="{ open }">
<div
class="flex items-center gap-2 p-2 rounded cursor-pointer transition-colors duration-150 ease-[cubic-bezier(0.4, 0, 0.2, 1)] hover:bg-surface-gray-2"
:class="{ 'bg-surface-white': open, 'px-1.5 ': isSidebarCollapsed }"
>
<TeamLogo
v-if="isSidebarCollapsed"
:team-name="userStore.currentTeam!.team_name"
:logo-url="userStore.currentTeam?.logo ?? null"
/>
<div v-else class="flex items-center gap-2 justify-between w-full">
<TeamSwitcherItem
:label="userStore.currentTeam!.team_name"
:logo-url="userStore.currentTeam?.logo ?? null"
/>
<ChevronsUpDown class="size-4 text-ink-gray-6" />
</div>
</div>
</template>
<template #item="{ item }">
<!-- @vue-expect-error -->
<TeamSwitcherItem
v-if="item.isTeam"
class="p-2 w-full hover:bg-surface-gray-2 cursor-pointer rounded"
:label="item.label"
:logo-url="item.logoUrl"
/>
<Button
v-else
variant="ghost"
@click="item.onClick"
:icon-left="item.icon"
class="w-full"
:label="item.label"
/>
</template>
</Dropdown>
</template>
18 changes: 18 additions & 0 deletions frontend/src/components/team/TeamSwitcherItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import TeamLogo from "./TeamLogo.vue";

type props = {
label: string;
logoUrl: string | null;
};

const props = defineProps<props>();
</script>
<template>
<div class="flex items-center gap-2 shrink-0">
<TeamLogo :team-name="label" :logo-url="logoUrl" />
Comment on lines +4 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix TypeScript error: string | null is not assignable to string | undefined.

The CI pipeline reports TS2322 on Line 13. The logoUrl prop is typed as string | null, but TeamLogo's logo-url prop likely expects string | undefined.

Proposed fix
-        <TeamLogo :team-name="label" :logo-url="logoUrl" />
+        <TeamLogo :team-name="label" :logo-url="logoUrl ?? undefined" />

Alternatively, align the prop type with TeamLogo's expectation by changing string | null to string | undefined on Line 6, if the parent components can accommodate that.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type props = {
label: string;
logoUrl: string | null;
};
const props = defineProps<props>();
</script>
<template>
<div class="flex items-center gap-2 shrink-0">
<TeamLogo :team-name="label" :logo-url="logoUrl" />
type props = {
label: string;
logoUrl: string | null;
};
const props = defineProps<props>();
</script>
<template>
<div class="flex items-center gap-2 shrink-0">
<TeamLogo :team-name="label" :logo-url="logoUrl ?? undefined" />
🧰 Tools
🪛 GitHub Actions: Frontend TypeScript

[error] 13-13: TS2322: Type 'string | null' is not assignable to type 'string | undefined'.

🤖 Prompt for AI Agents
In `@frontend/src/components/team/TeamSwitcherItem.vue` around lines 4 - 13, The
prop type for logoUrl in TeamSwitcherItem.vue is currently defined as string |
null which causes a TS2322 when passing it to the TeamLogo component that
expects string | undefined; update the props definition used by defineProps (the
props type near the type props = { label: string; logoUrl: ... } and the call
defineProps<props>()) to use string | undefined for logoUrl (or coerce/transform
null to undefined before passing to the TeamLogo component) so the TeamLogo
:logo-url binding receives the expected string | undefined type.

<span class="text-base font-medium text-ink-gray-7 whitespace-nowrap">
{{ label }}
</span>
</div>
</template>
Loading