diff --git a/packages/app/src/app/pages/NewDashboard/Sidebar/TeamAvatar.tsx b/packages/app/src/app/components/TeamAvatar/TeamAvatar.tsx
similarity index 73%
rename from packages/app/src/app/pages/NewDashboard/Sidebar/TeamAvatar.tsx
rename to packages/app/src/app/components/TeamAvatar/TeamAvatar.tsx
index 71c75c44d28..ab4fa59ff79 100644
--- a/packages/app/src/app/pages/NewDashboard/Sidebar/TeamAvatar.tsx
+++ b/packages/app/src/app/components/TeamAvatar/TeamAvatar.tsx
@@ -13,7 +13,17 @@ export const backgrounds = [
'blues.700',
];
-export const TeamAvatar = ({ name, size = 'big', ...props }) => {
+interface TeamAvatarProps {
+ name: string;
+ size?: 'small' | 'big';
+ className?: string;
+}
+
+export const TeamAvatar = ({
+ name,
+ size = 'big',
+ className,
+}: TeamAvatarProps) => {
if (!name) return null;
// consistent color
@@ -30,8 +40,10 @@ export const TeamAvatar = ({ name, size = 'big', ...props }) => {
textTransform: 'uppercase',
backgroundColor,
color: 'white',
+ fontWeight: 600,
+ fontFamily: 'Inter',
})}
- {...props}
+ className={className}
>
{name[0]}
diff --git a/packages/app/src/app/components/TeamAvatar/index.ts b/packages/app/src/app/components/TeamAvatar/index.ts
new file mode 100644
index 00000000000..8b7ed0b81eb
--- /dev/null
+++ b/packages/app/src/app/components/TeamAvatar/index.ts
@@ -0,0 +1 @@
+export { TeamAvatar } from './TeamAvatar';
diff --git a/packages/app/src/app/graphql/introspection-result.ts b/packages/app/src/app/graphql/introspection-result.ts
index aa8b944f6ca..6281c7307dd 100644
--- a/packages/app/src/app/graphql/introspection-result.ts
+++ b/packages/app/src/app/graphql/introspection-result.ts
@@ -9,7 +9,6 @@ export interface IntrospectionResultData {
}[];
};
}
-
const result: IntrospectionResultData = {
__schema: {
types: [
@@ -37,5 +36,4 @@ const result: IntrospectionResultData = {
],
},
};
-
export default result;
diff --git a/packages/app/src/app/graphql/types.ts b/packages/app/src/app/graphql/types.ts
index 560e5c8c64b..044139eb64a 100644
--- a/packages/app/src/app/graphql/types.ts
+++ b/packages/app/src/app/graphql/types.ts
@@ -1168,6 +1168,18 @@ export type TeamFragmentDashboardFragment = { __typename?: 'Team' } & Pick<
>;
};
+export type CurrentTeamInfoFragmentFragment = { __typename?: 'Team' } & Pick<
+ Team,
+ 'id' | 'creatorId' | 'description' | 'inviteToken' | 'name'
+> & {
+ users: Array<
+ { __typename?: 'User' } & Pick<
+ User,
+ 'avatarUrl' | 'name' | 'lastName' | 'username' | 'id'
+ >
+ >;
+ };
+
export type _CreateTeamMutationVariables = Exact<{
name: Scalars['String'];
}>;
@@ -1531,19 +1543,7 @@ export type GetTeamQueryVariables = Exact<{
export type GetTeamQuery = { __typename?: 'RootQueryType' } & {
me: Maybe<
{ __typename?: 'CurrentUser' } & {
- team: Maybe<
- { __typename?: 'Team' } & Pick<
- Team,
- 'id' | 'creatorId' | 'description' | 'inviteToken' | 'name'
- > & {
- users: Array<
- { __typename?: 'User' } & Pick<
- User,
- 'avatarUrl' | 'name' | 'lastName' | 'username' | 'id'
- >
- >;
- }
- >;
+ team: Maybe<{ __typename?: 'Team' } & CurrentTeamInfoFragmentFragment>;
}
>;
};
diff --git a/packages/app/src/app/overmind/actions.ts b/packages/app/src/app/overmind/actions.ts
index 181889e9c5a..8561a90c737 100755
--- a/packages/app/src/app/overmind/actions.ts
+++ b/packages/app/src/app/overmind/actions.ts
@@ -290,3 +290,17 @@ export const rejectTeamInvitation: Action<{ teamName: string }> = (
effects.notificationToast.success(`Rejected invitation to ${teamName}`);
};
+
+export const getActiveTeam: AsyncAction = async ({ state, effects }) => {
+ if (!state.activeTeam) return;
+
+ const team = await effects.gql.queries.getTeam({
+ teamId: state.activeTeam,
+ });
+
+ if (!team || !team.me) {
+ return;
+ }
+
+ state.activeTeamInfo = team.me.team;
+};
diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts
index 50ca534c4fb..7f1e8e34bbf 100644
--- a/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts
+++ b/packages/app/src/app/overmind/effects/gql/dashboard/fragments.ts
@@ -113,3 +113,20 @@ export const teamFragmentDashboard = gql`
}
}
`;
+
+export const currentTeamInfoFragment = gql`
+ fragment currentTeamInfoFragment on Team {
+ id
+ creatorId
+ description
+ inviteToken
+ name
+ users {
+ avatarUrl
+ name
+ lastName
+ username
+ id
+ }
+ }
+`;
diff --git a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts
index 0c7ae296110..049b9a7fc5c 100644
--- a/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts
+++ b/packages/app/src/app/overmind/effects/gql/dashboard/queries.ts
@@ -26,6 +26,7 @@ import {
sandboxFragmentDashboard,
sidebarCollectionDashboard,
templateFragmentDashboard,
+ currentTeamInfoFragment,
} from './fragments';
export const deletedSandboxes: Query<
@@ -202,19 +203,9 @@ export const getTeam: Query = gql`
query getTeam($teamId: ID!) {
me {
team(id: $teamId) {
- id
- creatorId
- description
- inviteToken
- name
- users {
- avatarUrl
- name
- lastName
- username
- id
- }
+ ...currentTeamInfoFragment
}
}
}
+ ${currentTeamInfoFragment}
`;
diff --git a/packages/app/src/app/overmind/factories.ts b/packages/app/src/app/overmind/factories.ts
index adffb8fe1fa..d24d2d098f4 100755
--- a/packages/app/src/app/overmind/factories.ts
+++ b/packages/app/src/app/overmind/factories.ts
@@ -66,16 +66,21 @@ export const withLoadApp = (
if (state.hasLogIn) {
try {
- state.user = await effects.api.getCurrentUser();
- actions.internal.setPatronPrice();
- effects.analytics.identify('signed_in', true);
- effects.analytics.setUserId(state.user.id, state.user.email);
const localStorageTeam = effects.browser.storage.get(
TEAM_ID_LOCAL_STORAGE
);
if (localStorageTeam) {
- state.dashboard.activeTeam = localStorageTeam;
+ state.activeTeam = localStorageTeam;
}
+ const [user] = await Promise.all([
+ effects.api.getCurrentUser(),
+ actions.getActiveTeam(),
+ ]);
+ state.user = user;
+ actions.internal.setPatronPrice();
+ effects.analytics.identify('signed_in', true);
+ effects.analytics.setUserId(state.user.id, state.user.email);
+
try {
actions.internal.trackCurrentTeams().catch(e => {});
actions.internal.identifyCurrentUser().catch(e => {});
diff --git a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts
index da2e7db19e1..ec70a433a1e 100755
--- a/packages/app/src/app/overmind/namespaces/dashboard/actions.ts
+++ b/packages/app/src/app/overmind/namespaces/dashboard/actions.ts
@@ -27,9 +27,9 @@ export const setActiveTeam: Action<{
id: string | null;
}> = ({ state, effects }, { id }) => {
// ignore if its already selected
- if (id === state.dashboard.activeTeam) return;
+ if (id === state.activeTeam) return;
- state.dashboard.activeTeam = id;
+ state.activeTeam = id;
effects.browser.storage.set(TEAM_ID_LOCAL_STORAGE, id);
state.dashboard.sandboxes = {
...state.dashboard.sandboxes,
@@ -139,35 +139,20 @@ export const getTeams: AsyncAction = async ({ state, effects }) => {
state.dashboard.teams = teams.me.teams;
};
-export const getTeam: AsyncAction = async ({ state, effects }) => {
- if (!state.dashboard.activeTeam) return;
- const team = await effects.gql.queries.getTeam({
- teamId: state.dashboard.activeTeam,
- });
-
- if (!team || !team.me) {
- return;
- }
-
- state.dashboard.activeTeamInfo = team.me.team;
-};
-
export const removeFromTeam: AsyncAction = async (
{ state, effects },
id
) => {
- if (!state.dashboard.activeTeam || !state.dashboard.activeTeamInfo) return;
+ if (!state.activeTeam || !state.activeTeamInfo) return;
try {
await effects.gql.mutations.removeFromTeam({
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
userId: id,
});
- state.dashboard.activeTeamInfo = {
- ...state.dashboard.activeTeamInfo,
- users: (state.dashboard.activeTeamInfo.users || []).filter(
- user => user.id !== id
- ),
+ state.activeTeamInfo = {
+ ...state.activeTeamInfo,
+ users: (state.activeTeamInfo.users || []).filter(user => user.id !== id),
};
} catch {
effects.notificationToast.error(
@@ -177,17 +162,17 @@ export const removeFromTeam: AsyncAction = async (
};
export const leaveTeam: AsyncAction = async ({ state, effects, actions }) => {
- if (!state.dashboard.activeTeam || !state.dashboard.activeTeamInfo) return;
+ if (!state.activeTeam || !state.activeTeamInfo) return;
try {
await effects.gql.mutations.leaveTeam({
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
});
actions.dashboard.setActiveTeam({ id: null });
actions.dashboard.getTeams();
effects.notificationToast.success(
- `You successfully left the ${state.dashboard.activeTeamInfo.name} team`
+ `You successfully left the ${state.activeTeamInfo.name} team`
);
} catch (e) {
effects.notificationToast.error(
@@ -200,20 +185,20 @@ export const inviteToTeam: AsyncAction = async (
{ state, effects },
value
) => {
- if (!state.dashboard.activeTeam) return;
+ if (!state.activeTeam) return;
const isEmail = value.includes('@');
try {
let data: any = null;
if (isEmail) {
const emailInvited = await effects.gql.mutations.inviteToTeamVieEmail({
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
email: value,
});
data = emailInvited.inviteToTeamViaEmail;
} else {
const usernameInvited = await effects.gql.mutations.inviteToTeam({
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
username: value,
});
@@ -253,8 +238,7 @@ export const getRecentSandboxes: AsyncAction = async ({ state, effects }) => {
dashboard.sandboxes[sandboxesTypes.RECENT] = data.me.sandboxes
.filter(
sandbox =>
- (sandbox.collection || { collection: {} }).teamId ===
- state.dashboard.activeTeam
+ (sandbox.collection || { collection: {} }).teamId === state.activeTeam
)
.slice(0, 50);
} catch (error) {
@@ -267,7 +251,7 @@ export const getRecentSandboxes: AsyncAction = async ({ state, effects }) => {
export const getAllFolders: AsyncAction = async ({ state, effects }) => {
try {
const data = await effects.gql.queries.getCollections({
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
});
if (!data || !data.me || !data.me.collections) {
return;
@@ -358,7 +342,7 @@ export const getDrafts: AsyncAction = async ({ state, effects }) => {
try {
const data = await effects.gql.queries.sandboxesByPath({
path: '/',
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
});
if (!data || !data.me || !data.me.collection) {
return;
@@ -383,7 +367,7 @@ export const getSandboxesByPath: AsyncAction = async (
try {
const data = await effects.gql.queries.sandboxesByPath({
path: '/' + path,
- teamId: state.dashboard.activeTeam,
+ teamId: state.activeTeam,
});
if (!data || !data.me || !data.me.collection) {
return;
@@ -421,10 +405,10 @@ export const getDeletedSandboxes: AsyncAction = async ({ state, effects }) => {
export const getTemplateSandboxes: AsyncAction = async ({ state, effects }) => {
const { dashboard } = state;
try {
- if (dashboard.activeTeam) {
+ if (state.activeTeam) {
dashboard.sandboxes[sandboxesTypes.TEMPLATES] = null;
const data = await effects.gql.queries.teamTemplates({
- id: dashboard.activeTeam,
+ id: state.activeTeam,
});
if (!data || !data.me || !data.me.team) {
@@ -866,8 +850,7 @@ export const getSearchSandboxes: AsyncAction = async (
.filter(x => !x.customTemplate)
.filter(
sandbox =>
- (sandbox.collection || { collection: {} }).teamId ===
- state.dashboard.activeTeam
+ (sandbox.collection || { collection: {} }).teamId === state.activeTeam
);
dashboard.sandboxes[sandboxesTypes.SEARCH] = sandboxesToShow;
@@ -955,7 +938,7 @@ export const setTeamInfo: AsyncAction<{
description: string | null;
}> = async ({ effects, state }, { name, description }) => {
const oldTeamInfo = state.dashboard.teams.find(team => team.name === name);
- const oldActiveTeamInfo = state.dashboard.activeTeamInfo;
+ const oldActiveTeamInfo = state.activeTeamInfo;
try {
await effects.gql.mutations.setTeamName({
name,
@@ -984,13 +967,17 @@ export const setTeamInfo: AsyncAction<{
return team;
});
- state.dashboard.activeTeamInfo = {
- ...oldActiveTeamInfo,
- name,
- description,
- };
+ if (oldActiveTeamInfo) {
+ state.activeTeamInfo = {
+ ...oldActiveTeamInfo,
+ name,
+ description,
+ };
+ }
} catch (e) {
- state.dashboard.activeTeamInfo = { ...oldActiveTeamInfo };
+ if (oldActiveTeamInfo) {
+ state.activeTeamInfo = { ...oldActiveTeamInfo };
+ }
if (state.dashboard.teams && oldTeamInfo) {
state.dashboard.teams = [...state.dashboard.teams, oldTeamInfo];
}
diff --git a/packages/app/src/app/overmind/namespaces/dashboard/state.ts b/packages/app/src/app/overmind/namespaces/dashboard/state.ts
index 96264ed8941..60b39fdd939 100755
--- a/packages/app/src/app/overmind/namespaces/dashboard/state.ts
+++ b/packages/app/src/app/overmind/namespaces/dashboard/state.ts
@@ -49,8 +49,6 @@ type State = {
};
teams: Array<{ __typename?: 'Team' } & Pick>;
allCollections: DELETE_ME_COLLECTION[] | null;
- activeTeam: string | null;
- activeTeamInfo: any | null;
selectedSandboxes: string[];
trashSandboxIds: string[];
isDragging: boolean;
@@ -89,8 +87,6 @@ export const state: State = {
},
viewMode: 'grid',
allCollections: null,
- activeTeam: null,
- activeTeamInfo: null,
teams: [],
recentSandboxesByTime: derived(({ sandboxes }: State) => {
const recentSandboxes = sandboxes.RECENT;
diff --git a/packages/app/src/app/overmind/state.ts b/packages/app/src/app/overmind/state.ts
index 7950533d4b4..05084cd83f6 100755
--- a/packages/app/src/app/overmind/state.ts
+++ b/packages/app/src/app/overmind/state.ts
@@ -4,6 +4,7 @@ import {
Sandbox,
UploadFile,
} from '@codesandbox/common/lib/types';
+import { CurrentTeamInfoFragmentFragment as CurrentTeam } from 'app/graphql/types';
import { derived } from 'overmind';
import { hasLogIn } from './utils/user';
@@ -19,6 +20,8 @@ type State = {
error: string | null;
contributors: string[];
user: CurrentUser | null;
+ activeTeam: string | null;
+ activeTeamInfo: CurrentTeam | null;
connected: boolean;
notifications: Notification[];
isLoadingCLI: boolean;
@@ -62,6 +65,8 @@ export const state: State = {
authToken: null,
error: null,
user: null,
+ activeTeam: null,
+ activeTeamInfo: null,
connected: true,
notifications: [],
contributors: [],
diff --git a/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxCard.tsx b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxCard.tsx
index f5f9dbb9f60..3a722692fa5 100644
--- a/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxCard.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxCard.tsx
@@ -9,11 +9,143 @@ import {
SkeletonText,
} from '@codesandbox/components';
import css from '@styled-system/css';
+import { SandboxItemComponentProps } from './types';
+
+const useImageLoaded = (url: string) => {
+ const [loaded, setLoaded] = React.useState(false);
+ React.useEffect(() => {
+ const img = new Image();
+ img.onload = () => {
+ setLoaded(true);
+ };
+
+ img.src = url;
+
+ if (img.complete) {
+ setLoaded(true);
+ }
+ }, [url]);
+
+ return loaded;
+};
+
+type SandboxTitleProps = { stoppedScrolling: boolean } & Pick<
+ SandboxItemComponentProps,
+ | 'editing'
+ | 'onContextMenu'
+ | 'onSubmit'
+ | 'onChange'
+ | 'onInputKeyDown'
+ | 'onInputBlur'
+ | 'PrivacyIcon'
+ | 'newTitle'
+ | 'sandboxTitle'
+>;
+
+const SandboxTitle: React.FC = React.memo(
+ ({
+ editing,
+ stoppedScrolling,
+ onContextMenu,
+ onSubmit,
+ onChange,
+ onInputKeyDown,
+ onInputBlur,
+ PrivacyIcon,
+ newTitle,
+ sandboxTitle,
+ }) => (
+
+ {editing ? (
+
+ ) : (
+
+
+
+ {sandboxTitle}
+
+
+ )}
+
+ {!stoppedScrolling ? (
+ // During scrolling we don't show the button, because it takes 1ms to render a button,
+ // while this doesn't sound like a lot, we need to render 4 new grid items when you scroll down,
+ // and this can't take more than 12ms. Showing another thing than the button shaves off a 4ms of
+ // render time and allows you to scroll with a minimum of 30fps.
+
+
+
+ ) : (
+
+ )}
+
+ )
+);
+
+type SandboxStatsProps = Pick<
+ SandboxItemComponentProps,
+ 'isHomeTemplate' | 'sandbox' | 'viewCount' | 'sandboxLocation' | 'lastUpdated'
+>;
+const SandboxStats: React.FC = React.memo(
+ ({ isHomeTemplate, sandbox, viewCount, sandboxLocation, lastUpdated }) => (
+
+
+
+
+ {viewCount}
+
+
+ {isHomeTemplate ? null : (
+ <>
+
+ •
+
+
+ {shortDistance(lastUpdated)}
+
+ >
+ )}
+ {sandboxLocation ? (
+ <>
+
+ •
+
+
+ {sandboxLocation}
+
+ >
+ ) : null}
+
+ )
+);
export const SandboxCard = ({
sandbox,
sandboxTitle,
sandboxLocation,
+ isHomeTemplate,
lastUpdated,
viewCount,
TemplateIcon,
@@ -37,16 +169,30 @@ export const SandboxCard = ({
thumbnailRef,
opacity,
...props
-}) => {
+}: SandboxItemComponentProps) => {
const [stoppedScrolling, setStoppedScrolling] = React.useState(false);
+ const [guaranteedScreenshotUrl, setGuaranteedScreenshotUrl] = React.useState<
+ string
+ >(screenshotUrl);
+
+ const imageLoaded = useImageLoaded(guaranteedScreenshotUrl);
+
React.useEffect(() => {
// We only want to render the screenshot once the user has stopped scrolling
if (!isScrolling && !stoppedScrolling) {
setStoppedScrolling(true);
}
}, [isScrolling, stoppedScrolling]);
- const showScreenshot = stoppedScrolling && screenshotUrl;
+
+ React.useEffect(() => {
+ // We always try to show the cached screenshot first, if someone looks at a sandbox we will try to
+ // generate a new one based on the latest contents.
+ const generateScreenshotUrl = `/api/v1/sandboxes/${sandbox.id}/screenshot.png`;
+ if (stoppedScrolling && !guaranteedScreenshotUrl) {
+ setGuaranteedScreenshotUrl(generateScreenshotUrl);
+ }
+ }, [stoppedScrolling, guaranteedScreenshotUrl, sandbox.id]);
return (
theme.speeds[4],
opacity,
':hover, :focus, :focus-within': {
@@ -90,10 +236,12 @@ export const SandboxCard = ({
},
})}
style={{
- [showScreenshot ? 'backgroundImage' : null]: `url(${screenshotUrl})`,
+ [imageLoaded
+ ? 'backgroundImage'
+ : null]: `url(${guaranteedScreenshotUrl})`,
}}
>
- {showScreenshot ? null : }
+ {imageLoaded ? null : }
-
- {editing ? (
-
- ) : (
-
-
-
- {sandboxTitle}
-
-
- )}
+
-
-
-
-
-
-
- {viewCount}
-
-
- {sandbox.isHomeTemplate ? null : (
- <>
-
- •
-
-
- {shortDistance(lastUpdated)}
-
- >
- )}
- {sandboxLocation ? (
- <>
-
- •
-
-
- {sandboxLocation}
-
- >
- ) : null}
-
+
);
};
diff --git a/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxListItem.tsx b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxListItem.tsx
index c9bb9041d4a..5f2325af6ec 100644
--- a/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxListItem.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/SandboxListItem.tsx
@@ -13,6 +13,7 @@ import {
Tooltip,
} from '@codesandbox/components';
import css from '@styled-system/css';
+import { SandboxItemComponentProps } from './types';
export const SandboxListItem = ({
sandbox,
@@ -40,7 +41,7 @@ export const SandboxListItem = ({
thumbnailRef,
opacity,
...props
-}) => (
+}: SandboxItemComponentProps) => (
null,
@@ -105,7 +106,8 @@ const GenericSandbox = ({ isScrolling, item }: GenericSandboxProps) => {
if (location.pathname.includes('deleted')) viewMode = 'list';
else viewMode = dashboard.viewMode;
- const Component = viewMode === 'list' ? SandboxListItem : SandboxCard;
+ const Component: React.FC =
+ viewMode === 'list' ? SandboxListItem : SandboxCard;
// interactions
const {
@@ -130,11 +132,14 @@ const GenericSandbox = ({ isScrolling, item }: GenericSandboxProps) => {
onSelectionClick(event, sandbox.id);
};
- const onContextMenu = event => {
- event.preventDefault();
- if (event.type === 'contextmenu') onRightClick(event, sandbox.id);
- else onMenuEvent(event, sandbox.id);
- };
+ const onContextMenu = React.useCallback(
+ event => {
+ event.preventDefault();
+ if (event.type === 'contextmenu') onRightClick(event, sandbox.id);
+ else onMenuEvent(event, sandbox.id);
+ },
+ [onRightClick, onMenuEvent, sandbox.id]
+ );
const history = useHistory();
const onDoubleClick = event => {
@@ -175,32 +180,41 @@ const GenericSandbox = ({ isScrolling, item }: GenericSandboxProps) => {
const [newTitle, setNewTitle] = React.useState(sandboxTitle);
- const onChange = (event: React.ChangeEvent) => {
- setNewTitle(event.target.value);
- };
- const onInputKeyDown = (event: React.KeyboardEvent) => {
- if (event.keyCode === ESC) {
- // Reset value and exit without saving
- setNewTitle(sandboxTitle);
- setRenaming(false);
- }
- };
+ const onChange = React.useCallback(
+ (event: React.ChangeEvent) => {
+ setNewTitle(event.target.value);
+ },
+ [setNewTitle]
+ );
+ const onInputKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.keyCode === ESC) {
+ // Reset value and exit without saving
+ setNewTitle(sandboxTitle);
+ setRenaming(false);
+ }
+ },
+ [setNewTitle, setRenaming, sandboxTitle]
+ );
- const onSubmit = async (event?: React.FormEvent) => {
- if (event) event.preventDefault();
- await actions.dashboard.renameSandbox({
- id: sandbox.id,
- title: newTitle,
- oldTitle: sandboxTitle,
- });
- setRenaming(false);
- track('Dashboard - Rename sandbox', { dashboardVersion: 2 });
- };
+ const onSubmit = React.useCallback(
+ async (event?: React.FormEvent) => {
+ if (event) event.preventDefault();
+ await actions.dashboard.renameSandbox({
+ id: sandbox.id,
+ title: newTitle,
+ oldTitle: sandboxTitle,
+ });
+ setRenaming(false);
+ track('Dashboard - Rename sandbox', { dashboardVersion: 2 });
+ },
+ [actions.dashboard, setRenaming, sandbox.id, newTitle, sandboxTitle]
+ );
- const onInputBlur = () => {
+ const onInputBlur = React.useCallback(() => {
// save value when you click outside or tab away
onSubmit();
- };
+ }, [onSubmit]);
const interactionProps = {
tabIndex: 0, // make div focusable
@@ -217,6 +231,7 @@ const GenericSandbox = ({ isScrolling, item }: GenericSandboxProps) => {
};
const sandboxProps = {
+ isHomeTemplate: item.isHomeTemplate,
sandboxTitle,
sandboxLocation,
lastUpdated,
diff --git a/packages/app/src/app/pages/NewDashboard/Components/Sandbox/types.ts b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/types.ts
new file mode 100644
index 00000000000..fcbceb58b75
--- /dev/null
+++ b/packages/app/src/app/pages/NewDashboard/Components/Sandbox/types.ts
@@ -0,0 +1,30 @@
+import { DashboardSandbox, DashboardTemplate } from '../../types';
+
+export interface SandboxItemComponentProps {
+ isHomeTemplate: boolean;
+ sandbox: DashboardSandbox['sandbox'] | DashboardTemplate['sandbox'];
+ sandboxTitle: string;
+ sandboxLocation: string;
+ lastUpdated: string;
+ viewCount: number | string;
+ TemplateIcon: React.FC<{ width: string; height: string }>;
+ PrivacyIcon: React.FC;
+ screenshotUrl: string | null;
+
+ isScrolling: boolean;
+ selected: boolean;
+ onClick: (evt: React.MouseEvent) => void;
+ onDoubleClick: (evt: React.MouseEvent) => void;
+ onBlur: (evt: React.FocusEvent) => void;
+ onContextMenu: (evt: React.MouseEvent) => void;
+
+ editing: boolean;
+ newTitle: string | null;
+ onChange: (evt: React.ChangeEvent) => void;
+ onInputKeyDown: (evt: React.KeyboardEvent) => void;
+ onSubmit: (evt: React.FormEvent) => void;
+ onInputBlur: (evt: React.FocusEvent) => void;
+
+ thumbnailRef: React.Ref;
+ opacity: number;
+}
diff --git a/packages/app/src/app/pages/NewDashboard/Components/Selection/index.tsx b/packages/app/src/app/pages/NewDashboard/Components/Selection/index.tsx
index 1ea6d3f90df..84bdba5a96d 100644
--- a/packages/app/src/app/pages/NewDashboard/Components/Selection/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Components/Selection/index.tsx
@@ -12,6 +12,7 @@ import {
ALT,
TAB,
} from '@codesandbox/common/lib/utils/keycodes';
+import { isEqual } from 'lodash-es';
import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator';
import { DragPreview } from './DragPreview';
import { ContextMenu } from './ContextMenu';
@@ -164,46 +165,63 @@ export const SelectionProvider: React.FC = ({
const [menuVisible, setMenuVisibility] = React.useState(false);
const [menuPosition, setMenuPosition] = React.useState({ x: 0, y: 0 });
- const onRightClick = (
- event: React.MouseEvent &
- React.KeyboardEvent,
- itemId: string
- ) => {
- if (!selectedIds.includes(itemId)) setSelectedIds([itemId]);
- setMenuVisibility(true);
- setMenuPosition({ x: event.clientX, y: event.clientY });
- };
+ const onRightClick = React.useCallback(
+ (
+ event: React.MouseEvent &
+ React.KeyboardEvent,
+ itemId: string
+ ) => {
+ setSelectedIds(s => {
+ if (!s.includes(itemId)) {
+ return [itemId];
+ }
+ return s;
+ });
- const onMenuEvent = (
- event:
- | React.MouseEvent
- | React.KeyboardEvent,
- itemId?: string
- ) => {
- if (itemId && !selectedIds.includes(itemId)) setSelectedIds([itemId]);
+ setMenuVisibility(true);
+ setMenuPosition({ x: event.clientX, y: event.clientY });
+ },
+ [setMenuVisibility, setMenuPosition]
+ );
- let menuElement: HTMLElement;
- if (event.type === 'click') {
- const target = event.target as HTMLButtonElement;
- menuElement = target;
- } else {
- // if the event is fired on the sandbox/folder, we find
- // the menu button to correctly position the menu
- const selectedItem = selectedIds[selectedIds.length - 1];
- menuElement = document.querySelector(
- `[data-selection-id="${selectedItem}"] button`
- );
- }
+ const onMenuEvent = React.useCallback(
+ (
+ event:
+ | React.MouseEvent
+ | React.KeyboardEvent,
+ itemId: string
+ ) => {
+ setSelectedIds(s => {
+ if (itemId && !s.includes(itemId)) {
+ return [itemId];
+ }
- const rect = menuElement.getBoundingClientRect();
- const position = {
- x: rect.x + rect.width / 2,
- y: rect.y + rect.height / 2,
- };
+ return s;
+ });
- setMenuVisibility(true);
- setMenuPosition(position);
- };
+ let menuElement: HTMLElement;
+ if (event.type === 'click') {
+ const target = event.target as HTMLButtonElement;
+ menuElement = target;
+ } else {
+ // if the event is fired on the sandbox/folder, we find
+ // the menu button to correctly position the menu
+ menuElement = document.querySelector(
+ `[data-selection-id="${itemId}"] button`
+ );
+ }
+
+ const rect = menuElement.getBoundingClientRect();
+ const position = {
+ x: rect.x + rect.width / 2,
+ y: rect.y + rect.height / 2,
+ };
+
+ setMenuVisibility(true);
+ setMenuPosition(position);
+ },
+ [setSelectedIds, setMenuVisibility, setMenuPosition]
+ );
const onBlur = (event: React.FocusEvent) => {
if (!event.bubbles) {
@@ -255,7 +273,8 @@ export const SelectionProvider: React.FC = ({
// disable selection keydowns when renaming
if (isRenaming) return;
- if (event.keyCode === ALT) onMenuEvent(event);
+ if (event.keyCode === ALT)
+ onMenuEvent(event, selectedIds[selectedIds.length - 1]);
// if only one thing is selected, open it
if (event.keyCode === ENTER && selectedIds.length === 1) {
@@ -509,8 +528,10 @@ export const SelectionProvider: React.FC = ({
overlappingIds.push(item.dataset.selectionId);
});
- setSelectedIds(overlappingIds);
- callbackCalledAt.current = new Date().getTime();
+ if (!isEqual(selectedIds, overlappingIds)) {
+ setSelectedIds(overlappingIds);
+ callbackCalledAt.current = new Date().getTime();
+ }
};
// performance hack: don't fire the callback again if it was fired 60ms ago
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/All/index.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/All/index.tsx
index 7be96e61eb3..579fee24669 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/All/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/All/index.tsx
@@ -20,7 +20,8 @@ export const AllPage = () => {
const {
actions,
state: {
- dashboard: { allCollections, sandboxes, activeTeam },
+ dashboard: { allCollections, sandboxes },
+ activeTeam,
},
} = useOvermind();
const [localTeam, setLocalTeam] = React.useState(activeTeam);
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/Deleted/index.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/Deleted/index.tsx
index 1ba55e994be..f725543e140 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/Deleted/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/Deleted/index.tsx
@@ -6,6 +6,7 @@ import { Header } from 'app/pages/NewDashboard/Components/Header';
import { VariableGrid } from 'app/pages/NewDashboard/Components/VariableGrid';
import { SelectionProvider } from 'app/pages/NewDashboard/Components/Selection';
import { DashboardGridItem } from 'app/pages/NewDashboard/types';
+import { SandboxFragmentDashboardFragment } from 'app/graphql/types';
import { getPossibleTemplates } from '../../utils';
export const Deleted = () => {
@@ -20,14 +21,17 @@ export const Deleted = () => {
actions.dashboard.getPage(sandboxesTypes.DELETED);
}, [actions.dashboard]);
- const getSection = (title, deletedSandboxes) => {
+ const getSection = (
+ title: string,
+ deletedSandboxes: SandboxFragmentDashboardFragment[]
+ ): DashboardGridItem[] => {
if (!deletedSandboxes.length) return [];
return [
{ type: 'header', title },
...deletedSandboxes.map(sandbox => ({
- type: 'sandbox',
- ...sandbox,
+ type: 'sandbox' as 'sandbox',
+ sandbox,
})),
];
};
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/Invite.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/Invite.tsx
index 8d5f23d6edf..5a6f65397df 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/Invite.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/Invite.tsx
@@ -20,9 +20,7 @@ import { Card } from './components';
export const Invite = () => {
const {
- state: {
- dashboard: { activeTeamInfo: team },
- },
+ state: { activeTeam, activeTeamInfo: team },
actions,
effects,
} = useOvermind();
@@ -30,10 +28,13 @@ export const Invite = () => {
const inviteLink = team && teamInviteLink(team.inviteToken);
React.useEffect(() => {
- actions.dashboard.getTeam();
- }, [actions.dashboard]);
+ actions.getActiveTeam();
+ }, [actions, activeTeam]);
const [inviteValue, setInviteValue] = React.useState('');
+ const [linkCopied, setLinkCopied] = React.useState(false);
+ const copyLinkTimeoutRef = React.useRef();
+
const [loading, setLoading] = React.useState(false);
const onSubmit = async event => {
@@ -46,6 +47,18 @@ export const Invite = () => {
if (!team) return null;
+ const copyLink = () => {
+ effects.browser.copyToClipboard(inviteLink);
+ setLinkCopied(true);
+
+ if (copyLinkTimeoutRef.current) {
+ window.clearTimeout(copyLinkTimeoutRef.current);
+ }
+ copyLinkTimeoutRef.current = window.setTimeout(() => {
+ setLinkCopied(false);
+ }, 1500);
+ };
+
return (
<>
@@ -95,13 +108,21 @@ export const Invite = () => {
})}
>
- {' '}
+ ) => {
+ if (evt.target) {
+ evt.target.select();
+ }
+ }}
+ type="text"
+ value={inviteLink}
+ />
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/TeamSettings.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/TeamSettings.tsx
index af5c8d1ed12..727538d2d7e 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/TeamSettings.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/TeamSettings.tsx
@@ -23,16 +23,13 @@ import { Card } from './components';
export const TeamSettings = () => {
const {
- state: {
- user: stateUser,
- dashboard: { activeTeamInfo: team },
- },
+ state: { user: stateUser, activeTeam, activeTeamInfo: team },
actions,
} = useOvermind();
useEffect(() => {
- actions.dashboard.getTeam();
- }, [actions.dashboard]);
+ actions.getActiveTeam();
+ }, [activeTeam, actions]);
const [editing, setEditing] = useState(false);
const [loading, setLoading] = useState(false);
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/index.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/index.tsx
index 69da70da403..8f9f9818684 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/Settings/index.tsx
@@ -15,7 +15,7 @@ export const Settings = () => {
if (location.pathname.includes('settings/new')) Component = NewTeam;
else if (location.pathname.includes('invite')) Component = Invite;
- else if (state.dashboard.activeTeam) Component = TeamSettings;
+ else if (state.activeTeam) Component = TeamSettings;
else Component = UserSettings;
return (
diff --git a/packages/app/src/app/pages/NewDashboard/Content/routes/Templates/index.tsx b/packages/app/src/app/pages/NewDashboard/Content/routes/Templates/index.tsx
index 642f544a53d..976d9329e73 100644
--- a/packages/app/src/app/pages/NewDashboard/Content/routes/Templates/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Content/routes/Templates/index.tsx
@@ -13,7 +13,8 @@ export const Templates = () => {
const {
actions,
state: {
- dashboard: { sandboxes, getFilteredSandboxes, activeTeam },
+ dashboard: { sandboxes, getFilteredSandboxes },
+ activeTeam,
},
} = useOvermind();
diff --git a/packages/app/src/app/pages/NewDashboard/Sidebar/index.tsx b/packages/app/src/app/pages/NewDashboard/Sidebar/index.tsx
index a78386d44b5..9d8fb2a2fae 100644
--- a/packages/app/src/app/pages/NewDashboard/Sidebar/index.tsx
+++ b/packages/app/src/app/pages/NewDashboard/Sidebar/index.tsx
@@ -24,7 +24,7 @@ import {
} from '@codesandbox/components';
import css from '@styled-system/css';
import merge from 'deepmerge';
-import { TeamAvatar } from './TeamAvatar';
+import { TeamAvatar } from 'app/components/TeamAvatar';
import { ContextMenu } from './ContextMenu';
export const SIDEBAR_WIDTH = 240;
@@ -32,14 +32,12 @@ export const SIDEBAR_WIDTH = 240;
const SidebarContext = React.createContext(null);
export const Sidebar = ({ visible, onSidebarToggle, ...props }) => {
- const {
- state: { dashboard, user },
- actions,
- } = useOvermind();
+ const { state, actions } = useOvermind();
const [activeAccount, setActiveAccount] = useState({
username: null,
avatarUrl: null,
});
+ const { dashboard, user } = state;
React.useEffect(() => {
actions.dashboard.getTeams();
@@ -47,13 +45,11 @@ export const Sidebar = ({ visible, onSidebarToggle, ...props }) => {
React.useEffect(() => {
actions.dashboard.getAllFolders();
- }, [actions.dashboard, dashboard.activeTeam]);
+ }, [actions.dashboard, state.activeTeam]);
React.useEffect(() => {
- if (dashboard.activeTeam) {
- const team = dashboard.teams.find(
- ({ id }) => id === dashboard.activeTeam
- );
+ if (state.activeTeam) {
+ const team = dashboard.teams.find(({ id }) => id === state.activeTeam);
if (team) setActiveAccount({ username: team.name, avatarUrl: null });
} else if (user) {
@@ -62,7 +58,7 @@ export const Sidebar = ({ visible, onSidebarToggle, ...props }) => {
avatarUrl: user.avatarUrl,
});
}
- }, [dashboard.activeTeam, dashboard.activeTeamInfo, dashboard.teams, user]);
+ }, [state.activeTeam, state.activeTeamInfo, dashboard.teams, user]);
const inTeamContext =
activeAccount && user && activeAccount.username !== user.username;
diff --git a/packages/app/src/app/pages/common/Modals/TeamInviteModal/index.tsx b/packages/app/src/app/pages/common/Modals/TeamInviteModal/index.tsx
index c72ce2b21c7..8c62547de35 100644
--- a/packages/app/src/app/pages/common/Modals/TeamInviteModal/index.tsx
+++ b/packages/app/src/app/pages/common/Modals/TeamInviteModal/index.tsx
@@ -9,7 +9,7 @@ import history from 'app/utils/history';
import { Element, Button, Text } from '@codesandbox/components';
import css from '@styled-system/css';
import { useMutation } from '@apollo/react-hooks';
-import { TeamAvatar } from 'app/pages/NewDashboard/Sidebar/TeamAvatar';
+import { TeamAvatar } from 'app/components/TeamAvatar';
export const TeamInviteModal = () => {
const {
diff --git a/packages/components/src/components/Button/index.tsx b/packages/components/src/components/Button/index.tsx
index b9b37eb53fd..1598f837b13 100644
--- a/packages/components/src/components/Button/index.tsx
+++ b/packages/components/src/components/Button/index.tsx
@@ -98,11 +98,6 @@ const commonStyles = {
},
};
-const merge = (...objs) =>
- objs.reduce(function mergeAll(merged, currentValue = {}) {
- return deepmerge(merged, currentValue);
- }, {});
-
export interface ButtonProps
extends React.ButtonHTMLAttributes,
IElementProps {
@@ -120,8 +115,7 @@ export const Button = React.forwardRef(
{ variant = 'primary', loading, css = {}, autoWidth, as: pAs, ...props },
ref
) {
- const styles = merge(variantStyles[variant], commonStyles, css);
-
+ const styles = deepmerge.all([variantStyles[variant], commonStyles, css]);
const usedAs = pAs || (props.to ? Link : 'button');
// default type is button unless props.as was changed
const type = usedAs === 'button' && 'button';
diff --git a/packages/components/src/components/ThemeProvider/index.tsx b/packages/components/src/components/ThemeProvider/index.tsx
index 95b167d4eb8..00441a93522 100644
--- a/packages/components/src/components/ThemeProvider/index.tsx
+++ b/packages/components/src/components/ThemeProvider/index.tsx
@@ -15,6 +15,7 @@ import designLanguage from '../../design-language/theme';
import VSCodeThemes from '../../themes';
import polyfillTheme from '../../utils/polyfill-theme';
import codesandboxBlack from '../../themes/codesandbox-black';
+import { TooltipStyles } from '../Tooltip';
export const getThemes = () => {
const results = VSCodeThemes.map(theme => ({
@@ -89,7 +90,10 @@ export const ThemeProvider = ({
return (
<>
- {children}
+
+
+ {children}
+
>
);
};
diff --git a/packages/components/src/components/Tooltip/index.tsx b/packages/components/src/components/Tooltip/index.tsx
index d0a8c53ff5b..d1b38302727 100644
--- a/packages/components/src/components/Tooltip/index.tsx
+++ b/packages/components/src/components/Tooltip/index.tsx
@@ -58,7 +58,7 @@ const animation = styledcss`
* so we apply global styles with their [data-reach-name]
*/
-const TooltipStyles = createGlobalStyle(
+export const TooltipStyles = createGlobalStyle(
css({
'[data-reach-tooltip][data-component=Tooltip]': {
backgroundColor: 'grays.900',
@@ -93,7 +93,6 @@ const Tooltip = props => {
return (
<>
-
{React.cloneElement(props.children, trigger)}