diff --git a/sdks/js/packages/core/package.json b/sdks/js/packages/core/package.json index 8d998730b..63290632a 100644 --- a/sdks/js/packages/core/package.json +++ b/sdks/js/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@raystack/frontier", - "version": "0.0.5", + "version": "0.0.6", "description": "A js library for frontier", "sideEffects": false, "main": "./dist/index.js", @@ -73,6 +73,7 @@ "axios": "^1.4.0", "class-variance-authority": "^0.7.0", "react-hook-form": "^7.45.2", + "react-loading-skeleton": "^3.3.1", "sonner": "^0.6.2", "yup": "^1.2.0" }, @@ -96,4 +97,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/sdks/js/packages/core/react/components/organization/domain/add-domain.tsx b/sdks/js/packages/core/react/components/organization/domain/add-domain.tsx index 39344693a..2ab511476 100644 --- a/sdks/js/packages/core/react/components/organization/domain/add-domain.tsx +++ b/sdks/js/packages/core/react/components/organization/domain/add-domain.tsx @@ -71,6 +71,7 @@ export const AddDomain = () => { cross navigate({ to: '/domains' })} diff --git a/sdks/js/packages/core/react/components/organization/domain/domain.columns.tsx b/sdks/js/packages/core/react/components/organization/domain/domain.columns.tsx index dc4f63791..baaa66de4 100644 --- a/sdks/js/packages/core/react/components/organization/domain/domain.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/domain/domain.columns.tsx @@ -5,8 +5,11 @@ import { useNavigate } from '@tanstack/react-router'; import { toast } from 'sonner'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1Domain } from '~/src'; +import Skeleton from 'react-loading-skeleton'; -export const columns: ColumnDef[] = [ +export const getColumns: ( + isLoading?: boolean +) => ColumnDef[] = isLoading => [ { accessorKey: 'name', meta: { @@ -14,17 +17,21 @@ export const columns: ColumnDef[] = [ paddingLeft: 0 } }, - cell: ({ row, getValue }) => { - return ( - - {row.original.name} - - ); - } + cell: isLoading + ? () => + : ({ row, getValue }) => { + return ( + + {row.original.name} + + ); + } }, { accessorKey: 'created_at', - cell: info => {info.getValue()} + cell: isLoading + ? () => + : info => {info.getValue()} }, { header: '', @@ -34,9 +41,11 @@ export const columns: ColumnDef[] = [ textAlign: 'end' } }, - cell: ({ row, getValue }) => ( - - ) + cell: isLoading + ? () => + : ({ row, getValue }) => ( + + ) } ]; diff --git a/sdks/js/packages/core/react/components/organization/domain/index.tsx b/sdks/js/packages/core/react/components/organization/domain/index.tsx index 80077a401..c2edea493 100644 --- a/sdks/js/packages/core/react/components/organization/domain/index.tsx +++ b/sdks/js/packages/core/react/components/organization/domain/index.tsx @@ -2,24 +2,34 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1Domain } from '~/src'; import { styles } from '../styles'; -import { columns } from './domain.columns'; +import { getColumns } from './domain.columns'; export default function Domain() { const { client, activeOrganization: organization } = useFrontier(); const routerState = useRouterState(); const [domains, setDomains] = useState([]); + const [isDomainsLoading, setIsDomainsLoading] = useState(false); const getDomains = useCallback(async () => { - if (!organization?.id) return; - const { - // @ts-ignore - data: { domains = [] } - } = await client?.frontierServiceListOrganizationDomains(organization?.id); - setDomains(domains); + try { + setIsDomainsLoading(true); + if (!organization?.id) return; + const { + // @ts-ignore + data: { domains = [] } + } = await client?.frontierServiceListOrganizationDomains( + organization?.id + ); + setDomains(domains); + } catch (err) { + console.error(err); + } finally { + setIsDomainsLoading(false); + } }, [client, organization?.id]); useEffect(() => { @@ -30,6 +40,16 @@ export default function Domain() { getDomains(); }, [client, getDomains, organization?.id]); + const updatedDomains = useMemo( + () => + isDomainsLoading + ? [{ id: 1 }, { id: 2 }, { id: 3 }] + : domains.length + ? domains + : [], + [isDomainsLoading, domains] + ); + return ( @@ -38,7 +58,8 @@ export default function Domain() { - + {/* @ts-ignore */} + @@ -61,13 +82,20 @@ const AllowedEmailDomains = () => { ); }; -const Domains = ({ domains }: { domains: V1Beta1Domain[] }) => { +const Domains = ({ + domains, + isLoading +}: { + domains: V1Beta1Domain[]; + isLoading?: boolean; +}) => { let navigate = useNavigate({ from: '/domains' }); const tableStyle = domains?.length ? { width: '100%' } : { width: '100%', height: '100%' }; + const columns = useMemo(() => getColumns(isLoading), [isLoading]); return ( { cross navigate({ to: '/domains' })} diff --git a/sdks/js/packages/core/react/components/organization/general/delete.tsx b/sdks/js/packages/core/react/components/organization/general/delete.tsx index 962583d8f..3f801bcf1 100644 --- a/sdks/js/packages/core/react/components/organization/general/delete.tsx +++ b/sdks/js/packages/core/react/components/organization/general/delete.tsx @@ -71,6 +71,7 @@ export const DeleteOrganization = () => { cross navigate({ to: '/' })} diff --git a/sdks/js/packages/core/react/components/organization/general/general.profile.tsx b/sdks/js/packages/core/react/components/organization/general/general.profile.tsx index 19506e2f5..6f07ece3a 100644 --- a/sdks/js/packages/core/react/components/organization/general/general.profile.tsx +++ b/sdks/js/packages/core/react/components/organization/general/general.profile.tsx @@ -1,27 +1,36 @@ 'use client'; import { Avatar, Flex, Text } from '@raystack/apsara'; +import Skeleton from 'react-loading-skeleton'; // @ts-ignore import { V1Beta1Organization } from '~/src'; import { getInitials } from '~/utils'; -import styles from './general.module.css'; +// import styles from './general.module.css'; interface GeneralProfileProps { organization?: V1Beta1Organization; + isLoading?: boolean; } -export const GeneralProfile = ({ organization }: GeneralProfileProps) => { +export const GeneralProfile = ({ + organization, + isLoading +}: GeneralProfileProps) => { return ( - - + {isLoading ? ( + + ) : ( + + )} + {/* Pick a logo for your organisation. Max size: 5 Mb - + */} ); }; diff --git a/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx b/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx index 8e231d88a..afdc32b12 100644 --- a/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx +++ b/sdks/js/packages/core/react/components/organization/general/general.workspace.tsx @@ -9,6 +9,7 @@ import { } from '@raystack/apsara'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import Skeleton from 'react-loading-skeleton'; import { toast } from 'sonner'; import * as yup from 'yup'; import { useFrontier } from '~/react/contexts/FrontierContext'; @@ -22,9 +23,11 @@ const generalSchema = yup .required(); export const GeneralOrganization = ({ - organization + organization, + isLoading }: { organization?: V1Beta1Organization; + isLoading?: boolean; }) => { const { client } = useFrontier(); const { @@ -60,19 +63,23 @@ export const GeneralOrganization = ({ - ( - - )} - defaultValue={organization?.title} - control={control} - name="title" - /> + {isLoading ? ( + + ) : ( + ( + + )} + defaultValue={organization?.title} + control={control} + name="title" + /> + )} {errors.title && String(errors.title?.message)} @@ -81,19 +88,23 @@ export const GeneralOrganization = ({ - ( - - )} - defaultValue={organization?.name} - control={control} - name="name" - /> + {isLoading ? ( + + ) : ( + ( + + )} + defaultValue={organization?.name} + control={control} + name="name" + /> + )} {errors.name && String(errors.name?.message)} @@ -105,6 +116,7 @@ export const GeneralOrganization = ({ variant="primary" type="submit" style={{ width: 'fit-content' }} + disabled={isLoading || isSubmitting} > {isSubmitting ? 'updating...' : 'Update'} diff --git a/sdks/js/packages/core/react/components/organization/general/index.tsx b/sdks/js/packages/core/react/components/organization/general/index.tsx index be151b2f1..7de8aa38b 100644 --- a/sdks/js/packages/core/react/components/organization/general/index.tsx +++ b/sdks/js/packages/core/react/components/organization/general/index.tsx @@ -8,25 +8,35 @@ import { GeneralProfile } from './general.profile'; import { GeneralOrganization } from './general.workspace'; export default function GeneralSetting() { - const { activeOrganization: organization } = useFrontier(); + const { + activeOrganization: organization, + isActiveOrganizationLoading: isLoading + } = useFrontier(); return ( General - + - + - + ); } -export const GeneralDeleteOrganization = () => { +export const GeneralDeleteOrganization = ({ + isLoading +}: { + isLoading?: boolean; +}) => { const navigate = useNavigate({ from: '/' }); return ( @@ -39,6 +49,7 @@ export const GeneralDeleteOrganization = () => { type="submit" size="medium" onClick={() => navigate({ to: '/delete' })} + disabled={isLoading} > Delete organization diff --git a/sdks/js/packages/core/react/components/organization/members/index.tsx b/sdks/js/packages/core/react/components/organization/members/index.tsx index 902cf9893..ed580bcf9 100644 --- a/sdks/js/packages/core/react/components/organization/members/index.tsx +++ b/sdks/js/packages/core/react/components/organization/members/index.tsx @@ -2,7 +2,7 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1User } from '~/src'; import { styles } from '../styles'; @@ -11,30 +11,38 @@ import type { MembersTableType } from './member.types'; export default function WorkspaceMembers() { const [users, setUsers] = useState([]); + const [isUsersLoading, setIsUsersLoading] = useState(false); const { client, activeOrganization: organization } = useFrontier(); const routerState = useRouterState(); const fetchOrganizationUser = useCallback(async () => { if (!organization?.id) return; - const { - // @ts-ignore - data: { users } - } = await client?.frontierServiceListOrganizationUsers(organization?.id); - setUsers(users); + try { + setIsUsersLoading(true); + const { + // @ts-ignore + data: { users } + } = await client?.frontierServiceListOrganizationUsers(organization?.id); + setUsers(users); + + const { + // @ts-ignore + data: { invitations } + } = await client?.frontierServiceListOrganizationInvitations( + organization?.id + ); - const { + const invitedUsers = invitations.map((user: V1Beta1User) => ({ + ...user, + invited: true + })); // @ts-ignore - data: { invitations } - } = await client?.frontierServiceListOrganizationInvitations( - organization?.id - ); - - const invitedUsers = invitations.map((user: V1Beta1User) => ({ - ...user, - invited: true - })); - // @ts-ignore - setUsers([...users, ...invitedUsers]); + setUsers([...users, ...invitedUsers]); + } catch (err) { + console.error(err); + } finally { + setIsUsersLoading(false); + } }, [client, organization?.id]); useEffect(() => { @@ -45,6 +53,16 @@ export default function WorkspaceMembers() { fetchOrganizationUser(); }, [fetchOrganizationUser, routerState.location.key]); + const updatedUsers = useMemo( + () => + isUsersLoading + ? [{ id: 1 }, { id: 2 }, { id: 3 }] + : users.length + ? users + : [], + [isUsersLoading, users] + ); + return ( @@ -53,7 +71,14 @@ export default function WorkspaceMembers() { - + {organization?.id ? ( + + ) : null} @@ -72,20 +97,29 @@ const ManageMembers = () => ( ); -const MembersTable = ({ users, organizationId }: MembersTableType) => { +const MembersTable = ({ + users, + organizationId, + isLoading +}: MembersTableType) => { let navigate = useNavigate({ from: '/members' }); const tableStyle = users?.length ? { width: '100%' } : { width: '100%', height: '100%' }; + const columns = useMemo( + () => getColumns(organizationId, isLoading), + [organizationId, isLoading] + ); + return ( { cross navigate({ to: '/members' })} diff --git a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx index e3d677b4b..bc1597e9a 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.columns.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.columns.tsx @@ -6,10 +6,12 @@ import { toast } from 'sonner'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1User } from '~/src'; import { getInitials } from '~/utils'; +import Skeleton from 'react-loading-skeleton'; export const getColumns: ( - id: string -) => ColumnDef[] = organizationId => [ + id: string, + isLoading?: boolean +) => ColumnDef[] = (organizationId, isLoading) => [ { header: '', accessorKey: 'profile_picture', @@ -20,15 +22,18 @@ export const getColumns: ( padding: 0 } }, - cell: ({ row, getValue }) => { - return ( - - ); - } + cell: isLoading + ? () => + : ({ row, getValue }) => { + return ( + + ); + } }, { accessorKey: 'title', @@ -37,21 +42,28 @@ export const getColumns: ( paddingLeft: 0 } }, - cell: ({ row, getValue }) => { - return ( - - - {row.original.email} - - ); - } + cell: isLoading + ? () => + : ({ row, getValue }) => { + return ( + + + {row.original.email} + + ); + } }, { accessorKey: 'email', - cell: ({ row, getValue }) => { - // @ts-ignore - return {getValue() || row.original?.user_id}; - } + meta: {}, + cell: isLoading + ? () => + : ({ row, getValue }) => { + return ( + // @ts-ignore + {getValue() || row.original?.user_id} + ); + } }, { header: '', @@ -61,12 +73,14 @@ export const getColumns: ( textAlign: 'end' } }, - cell: ({ row }) => ( - - ) + cell: isLoading + ? () => + : ({ row }) => ( + + ) } ]; diff --git a/sdks/js/packages/core/react/components/organization/members/member.types.tsx b/sdks/js/packages/core/react/components/organization/members/member.types.tsx index 010263afe..027e87aea 100644 --- a/sdks/js/packages/core/react/components/organization/members/member.types.tsx +++ b/sdks/js/packages/core/react/components/organization/members/member.types.tsx @@ -10,5 +10,6 @@ export enum MemberActionmethods { export type MembersTableType = { users: User[]; - organizationId?: string; + organizationId: string; + isLoading?: boolean; }; diff --git a/sdks/js/packages/core/react/components/organization/profile.tsx b/sdks/js/packages/core/react/components/organization/profile.tsx index fca38291c..df377b254 100644 --- a/sdks/js/packages/core/react/components/organization/profile.tsx +++ b/sdks/js/packages/core/react/components/organization/profile.tsx @@ -174,15 +174,28 @@ export const OrganizationProfile = ({ organizationId, defaultRoute = '/' }: OrganizationProfileProps) => { - const { client, setActiveOrganization } = useFrontier(); + const { client, setActiveOrganization, setIsActiveOrganizationLoading } = + useFrontier(); const fetchOrganization = useCallback(async () => { - const { - // @ts-ignore - data: { organization } - } = await client?.frontierServiceGetOrganization(organizationId); - setActiveOrganization(organization); - }, [client, organizationId, setActiveOrganization]); + try { + setIsActiveOrganizationLoading(true); + const { + // @ts-ignore + data: { organization } + } = await client?.frontierServiceGetOrganization(organizationId); + setActiveOrganization(organization); + } catch (err) { + console.error(err); + } finally { + setIsActiveOrganizationLoading(false); + } + }, [ + client, + organizationId, + setActiveOrganization, + setIsActiveOrganizationLoading + ]); useEffect(() => { if (organizationId) { diff --git a/sdks/js/packages/core/react/components/organization/project/add.tsx b/sdks/js/packages/core/react/components/organization/project/add.tsx index 4f2ba6c35..a25193183 100644 --- a/sdks/js/packages/core/react/components/organization/project/add.tsx +++ b/sdks/js/packages/core/react/components/organization/project/add.tsx @@ -64,8 +64,13 @@ export const AddProject = () => { Add Project - {/* @ts-ignore */} - cross navigate('/members')} /> + cross navigate({ to: '/members' })} + style={{ cursor: 'pointer' }} + />
diff --git a/sdks/js/packages/core/react/components/organization/project/delete.tsx b/sdks/js/packages/core/react/components/organization/project/delete.tsx index 9a208659f..85dd2bd14 100644 --- a/sdks/js/packages/core/react/components/organization/project/delete.tsx +++ b/sdks/js/packages/core/react/components/organization/project/delete.tsx @@ -89,8 +89,13 @@ export const DeleteProject = () => { Verify project deletion - {/* @ts-ignore */} - cross navigate('/')} /> + cross navigate({ to: '/' })} + style={{ cursor: 'pointer' }} + /> diff --git a/sdks/js/packages/core/react/components/organization/project/index.tsx b/sdks/js/packages/core/react/components/organization/project/index.tsx index 16bc7f471..f977ce582 100644 --- a/sdks/js/packages/core/react/components/organization/project/index.tsx +++ b/sdks/js/packages/core/react/components/organization/project/index.tsx @@ -2,23 +2,31 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1Project } from '~/src'; import { styles } from '../styles'; -import { columns } from './projects.columns'; +import { getColumns } from './projects.columns'; export default function WorkspaceProjects() { const { client, activeOrganization: organization } = useFrontier(); const routerState = useRouterState(); const [projects, setProjects] = useState([]); + const [isProjectsLoading, setIsProjectsLoading] = useState(false); const getProjects = useCallback(async () => { - const { - // @ts-ignore - data: { projects = [] } - } = await client?.adminServiceListProjects({ orgId: organization?.id }); - setProjects(projects); + try { + setIsProjectsLoading(true); + const { + // @ts-ignore + data: { projects = [] } + } = await client?.adminServiceListProjects({ orgId: organization?.id }); + setProjects(projects); + } catch (err) { + console.error(err); + } finally { + setIsProjectsLoading(false); + } }, [client, organization?.id]); useEffect(() => { @@ -29,6 +37,15 @@ export default function WorkspaceProjects() { getProjects(); }, [client, getProjects, organization?.id]); + const updatedProjects = useMemo( + () => + isProjectsLoading + ? [{ id: 1 }, { id: 2 }, { id: 3 }] + : projects.length + ? projects + : [], + [isProjectsLoading, projects] + ); return ( @@ -36,7 +53,11 @@ export default function WorkspaceProjects() { - + @@ -46,15 +67,17 @@ export default function WorkspaceProjects() { interface WorkspaceProjectsProps { projects: V1Beta1Project[]; + isLoading?: boolean; } -const ProjectsTable = ({ projects }: WorkspaceProjectsProps) => { +const ProjectsTable = ({ projects, isLoading }: WorkspaceProjectsProps) => { let navigate = useNavigate({ from: '/projects' }); const tableStyle = projects?.length ? { width: '100%' } : { width: '100%', height: '100%' }; + const columns = useMemo(() => getColumns(isLoading), [isLoading]); return ( [] = [ +export const getColumns: ( + isLoading?: boolean +) => ColumnDef[] = isLoading => [ { accessorKey: 'name', - cell: ({ row, getValue }) => { - return ( - - {getValue()} - - ); - } + cell: isLoading + ? () => + : ({ row, getValue }) => { + return ( + + {getValue()} + + ); + } }, { accessorKey: 'privacy', - cell: info => {info.getValue() ?? 'Public'} + cell: isLoading + ? () => + : info => {info.getValue() ?? 'Public'} }, { accessorKey: 'members', - cell: ({ row, getValue }) => + cell: isLoading + ? () => + : ({ row, getValue }) => }, { header: '', @@ -43,9 +55,11 @@ export const columns: ColumnDef[] = [ textAlign: 'end' } }, - cell: ({ row, getValue }) => ( - - ) + cell: isLoading + ? () => + : ({ row, getValue }) => ( + + ) } ]; diff --git a/sdks/js/packages/core/react/components/organization/teams/add.tsx b/sdks/js/packages/core/react/components/organization/teams/add.tsx index 4b44f5548..556935403 100644 --- a/sdks/js/packages/core/react/components/organization/teams/add.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/add.tsx @@ -64,6 +64,7 @@ export const AddTeam = () => { // @ts-ignore src={cross} onClick={() => navigate({ to: '/teams' })} + style={{ cursor: 'pointer' }} /> diff --git a/sdks/js/packages/core/react/components/organization/teams/delete.tsx b/sdks/js/packages/core/react/components/organization/teams/delete.tsx index 04f994318..9fc1df226 100644 --- a/sdks/js/packages/core/react/components/organization/teams/delete.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/delete.tsx @@ -89,9 +89,10 @@ export const DeleteTeam = () => { cross navigate({ to: '/teams' })} + style={{ cursor: 'pointer' }} /> diff --git a/sdks/js/packages/core/react/components/organization/teams/index.tsx b/sdks/js/packages/core/react/components/organization/teams/index.tsx index e1c90adcf..3ce88a675 100644 --- a/sdks/js/packages/core/react/components/organization/teams/index.tsx +++ b/sdks/js/packages/core/react/components/organization/teams/index.tsx @@ -2,28 +2,36 @@ import { Button, DataTable, EmptyState, Flex, Text } from '@raystack/apsara'; import { Outlet, useNavigate, useRouterState } from '@tanstack/react-router'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFrontier } from '~/react/contexts/FrontierContext'; import { V1Beta1Group } from '~/src'; import { styles } from '../styles'; -import { columns } from './teams.columns'; +import { getColumns } from './teams.columns'; interface WorkspaceTeamProps { teams: V1Beta1Group[]; + isLoading?: boolean; } export default function WorkspaceTeams() { const [teams, setTeams] = useState([]); - + const [isTeamsLoading, setIsTeamsLoading] = useState(false); const { client, activeOrganization: organization } = useFrontier(); const routerState = useRouterState(); const getTeams = useCallback(async () => { - const { - // @ts-ignore - data: { groups = [] } - } = await client?.adminServiceListGroups({ orgId: organization?.id }); - setTeams(groups); + try { + setIsTeamsLoading(true); + const { + // @ts-ignore + data: { groups = [] } + } = await client?.adminServiceListGroups({ orgId: organization?.id }); + setTeams(groups); + } catch (err) { + console.error(err); + } finally { + setIsTeamsLoading(false); + } }, [client, organization?.id]); useEffect(() => { @@ -34,6 +42,16 @@ export default function WorkspaceTeams() { getTeams(); }, [client, getTeams, organization?.id]); + const updatedTeams = useMemo( + () => + isTeamsLoading + ? [{ id: 1 }, { id: 2 }, { id: 3 }] + : teams.length + ? teams + : [], + [isTeamsLoading, teams] + ); + return ( @@ -41,7 +59,8 @@ export default function WorkspaceTeams() { - + {/* @ts-ignore */} + @@ -49,13 +68,15 @@ export default function WorkspaceTeams() { ); } -const TeamsTable = ({ teams }: WorkspaceTeamProps) => { +const TeamsTable = ({ teams, isLoading }: WorkspaceTeamProps) => { let navigate = useNavigate({ from: '/members' }); const tableStyle = teams?.length ? { width: '100%' } : { width: '100%', height: '100%' }; + const columns = useMemo(() => getColumns(isLoading), [isLoading]); + return ( [] = [ +export const getColumns: ( + isLoading?: boolean +) => ColumnDef[] = isLoading => [ { header: 'Title', accessorKey: 'name', - cell: ({ row, getValue }) => ( - - {getValue()} - - ) + cell: isLoading + ? () => + : ({ row, getValue }) => ( + + {getValue()} + + ) }, { accessorKey: 'members', - cell: ({ row, getValue }) => ( - - ) + cell: isLoading + ? () => + : ({ row, getValue }) => ( + + ) }, { header: '', @@ -40,9 +47,11 @@ export const columns: ColumnDef[] = [ textAlign: 'end' } }, - cell: ({ row, getValue }) => ( - - ) + cell: isLoading + ? () => + : ({ row, getValue }) => ( + + ) } ]; diff --git a/sdks/js/packages/core/react/components/organization/user/avatar.tsx b/sdks/js/packages/core/react/components/organization/user/avatar.tsx index 3a4df941a..b6617a5b7 100644 --- a/sdks/js/packages/core/react/components/organization/user/avatar.tsx +++ b/sdks/js/packages/core/react/components/organization/user/avatar.tsx @@ -1,23 +1,28 @@ 'use client'; import { Avatar, Flex, Text } from '@raystack/apsara'; +import Skeleton from 'react-loading-skeleton'; import { useFrontier } from '~/react/contexts/FrontierContext'; - import { getInitials } from '~/utils'; export const GeneralProfile = () => { - const { user } = useFrontier(); + const { user, isUserLoading: isLoading } = useFrontier(); return ( - - + {isLoading ? ( + + ) : ( + + )} + + {/* Pick a profile picture for your avatar. Max size: 5 Mb - + */} ); }; diff --git a/sdks/js/packages/core/react/components/organization/user/update.tsx b/sdks/js/packages/core/react/components/organization/user/update.tsx index a0c1e8216..638ebd071 100644 --- a/sdks/js/packages/core/react/components/organization/user/update.tsx +++ b/sdks/js/packages/core/react/components/organization/user/update.tsx @@ -9,6 +9,7 @@ import { } from '@raystack/apsara'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import Skeleton from 'react-loading-skeleton'; import { toast } from 'sonner'; import * as yup from 'yup'; import { useFrontier } from '~/react/contexts/FrontierContext'; @@ -21,7 +22,7 @@ const generalSchema = yup .required(); export const UpdateProfile = () => { - const { client, user } = useFrontier(); + const { client, user, isUserLoading: isLoading } = useFrontier(); const { reset, control, @@ -55,20 +56,23 @@ export const UpdateProfile = () => { - ( - - )} - defaultValue={user?.title} - control={control} - name="title" - /> - + {isLoading ? ( + + ) : ( + ( + + )} + defaultValue={user?.title} + control={control} + name="title" + /> + )} {errors.title && String(errors.title?.message)} @@ -76,22 +80,26 @@ export const UpdateProfile = () => { - ( - - )} - defaultValue={user?.name} - control={control} - name="email" - /> + {isLoading ? ( + + ) : ( + ( + + )} + defaultValue={user?.name} + control={control} + name="email" + /> + )} {errors.title && String(errors.title?.message)} @@ -103,6 +111,7 @@ export const UpdateProfile = () => { variant="primary" type="submit" style={{ width: 'fit-content' }} + disabled={isLoading || isSubmitting} > {isSubmitting ? 'updating...' : 'Update'} diff --git a/sdks/js/packages/core/react/components/window/index.tsx b/sdks/js/packages/core/react/components/window/index.tsx index 171c26c89..c76ae0974 100644 --- a/sdks/js/packages/core/react/components/window/index.tsx +++ b/sdks/js/packages/core/react/components/window/index.tsx @@ -14,8 +14,6 @@ interface WindowProps extends React.HTMLAttributes { children?: React.ReactNode; } - - export const Window = ({ open = false, onOpenChange, @@ -49,6 +47,7 @@ export const Window = ({ // @ts-ignore src={isCloseActive ? closeClose : closeDefault} onClick={() => onOpenChange && onOpenChange(false)} + style={{ cursor: 'pointer' }} /> setZoomActive(true)} @@ -63,6 +62,7 @@ export const Window = ({ : resizeDefault } onClick={() => setZoom(!zoom)} + style={{ cursor: 'pointer' }} /> diff --git a/sdks/js/packages/core/react/contexts/FrontierContext.tsx b/sdks/js/packages/core/react/contexts/FrontierContext.tsx index 089394bff..4de8f0fb1 100644 --- a/sdks/js/packages/core/react/contexts/FrontierContext.tsx +++ b/sdks/js/packages/core/react/contexts/FrontierContext.tsx @@ -40,6 +40,12 @@ interface FrontierContextProviderProps { setActiveOrganization: Dispatch< SetStateAction >; + + isActiveOrganizationLoading: boolean; + setIsActiveOrganizationLoading: Dispatch>; + + isUserLoading: boolean; + setIsUserLoading: Dispatch>; } const defaultConfig = { @@ -67,7 +73,13 @@ const initialValues: FrontierContextProviderProps = { setUser: () => undefined, activeOrganization: undefined, - setActiveOrganization: () => undefined + setActiveOrganization: () => undefined, + + isUserLoading: false, + setIsUserLoading: () => undefined, + + isActiveOrganizationLoading: false, + setIsActiveOrganizationLoading: () => undefined }; export const FrontierContext = @@ -88,6 +100,9 @@ export const FrontierContextProvider = ({ const [user, setUser] = useState(); const [activeOrganization, setActiveOrganization] = useState(); + const [isActiveOrganizationLoading, setIsActiveOrganizationLoading] = + useState(false); + const [isUserLoading, setIsUserLoading] = useState(false); useEffect(() => { async function getFrontierInformation() { @@ -108,6 +123,7 @@ export const FrontierContextProvider = ({ useEffect(() => { async function getFrontierCurrentUser() { try { + setIsUserLoading(true); const { data: { user } } = await frontierClient.frontierServiceGetCurrentUser(); @@ -116,10 +132,12 @@ export const FrontierContextProvider = ({ console.error( 'frontier:sdk:: There is problem with fetching current user information' ); + } finally { + setIsUserLoading(false); } } getFrontierCurrentUser(); - }, []); + }, [frontierClient]); useEffect(() => { async function getFrontierCurrentUserGroups() { @@ -173,7 +191,11 @@ export const FrontierContextProvider = ({ user, setUser, activeOrganization, - setActiveOrganization + setActiveOrganization, + isActiveOrganizationLoading, + setIsActiveOrganizationLoading, + isUserLoading, + setIsUserLoading }} > {children} diff --git a/sdks/js/packages/core/react/index.ts b/sdks/js/packages/core/react/index.ts index d6afa5016..705ebc8f9 100644 --- a/sdks/js/packages/core/react/index.ts +++ b/sdks/js/packages/core/react/index.ts @@ -1,4 +1,5 @@ import '@raystack/apsara/index.css'; +import 'react-loading-skeleton/dist/skeleton.css'; export { Container } from './components/Container'; export { Header } from './components/Header'; @@ -11,4 +12,3 @@ export { Window } from './components/window'; export { useFrontier } from './contexts/FrontierContext'; export { FrontierProvider } from './contexts/FrontierProvider'; - diff --git a/sdks/js/pnpm-lock.yaml b/sdks/js/pnpm-lock.yaml index 1d22b0ece..9fdc51e93 100644 --- a/sdks/js/pnpm-lock.yaml +++ b/sdks/js/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: react-hook-form: specifier: ^7.45.2 version: 7.45.2(react@18.2.0) + react-loading-skeleton: + specifier: ^3.3.1 + version: 3.3.1(react@18.2.0) sonner: specifier: ^0.6.2 version: 0.6.2(react-dom@18.2.0)(react@18.2.0) @@ -5457,6 +5460,14 @@ packages: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false + /react-loading-skeleton@3.3.1(react@18.2.0): + resolution: {integrity: sha512-NilqqwMh2v9omN7LteiDloEVpFyMIa0VGqF+ukqp0ncVlYu1sKYbYGX9JEl+GtOT9TKsh04zCHAbavnQ2USldA==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'}