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
3 changes: 3 additions & 0 deletions sdks/js/packages/core/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export {
createConnectQueryKey
} from '@connectrpc/connect-query';

// Re-export Connect utilities
export { ConnectError, Code } from '@connectrpc/connect';

// Re-export Frontier service queries for convenience
export { FrontierServiceQueries } from '@raystack/proton/frontier';

Expand Down
70 changes: 33 additions & 37 deletions sdks/js/packages/core/react/components/organization/project/add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,20 @@ import { useNavigate } from '@tanstack/react-router';
import { useForm } from 'react-hook-form';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { useMutation } from '@connectrpc/connect-query';
import { FrontierServiceQueries, CreateProjectRequestSchema } from '@raystack/proton/frontier';
import {
FrontierServiceQueries,
CreateProjectRequestSchema
} from '@raystack/proton/frontier';
import { create } from '@bufbuild/protobuf';
import cross from '~/react/assets/cross.svg';
import styles from '../organization.module.css';
import slugify from 'slugify';
import { generateHashFromString } from '~/react/utils';
import { ConnectError, Code } from '@connectrpc/connect';

const projectSchema = yup
.object({
title: yup.string().required(),
name: yup
.string()
.required('name is a required field')
.min(3, 'name is not valid, Min 3 characters allowed')
.max(50, 'name is not valid, Max 50 characters allowed')
.matches(
/^[a-zA-Z0-9_-]{3,50}$/,
"Only numbers, letters, '-', and '_' are allowed. Spaces are not allowed."
),
org_id: yup.string().required()
})
.required();
Expand Down Expand Up @@ -61,32 +58,38 @@ export const AddProject = () => {
onSuccess: () => {
toast.success('Project added');
navigate({ to: '/projects' });
},
onError: (error: Error) => {
if (error instanceof Response && error?.status === 409) {
setError('name', {
message: 'Project name already exists. Please enter a unique name.'
});
} else {
toast.error('Something went wrong', {
description: error.message || 'Failed to create project'
});
}
}
}
);

async function onSubmit(data: FormData) {
if (!organization?.id) return;
await createProject(
create(CreateProjectRequestSchema, {
body: {
title: data.title,
name: data.name,
orgId: organization.id
}
})
);
const slug = slugify(data.title, { lower: true, strict: true });
const suffix = generateHashFromString(organization.id);
const name = `${slug}-${suffix}`;
try {
await createProject(
create(CreateProjectRequestSchema, {
body: {
title: data.title,
name,
orgId: organization.id
}
})
);
} catch (error) {
if (error instanceof ConnectError && error.code === Code.AlreadyExists) {
setError('title', {
message:
'A project with a similar title already exist. Please tweak the title and try again.'
});
} else {
toast.error('Something went wrong', {
description:
error instanceof Error ? error.message : 'Failed to create project'
});
}
}
}

return (
Expand Down Expand Up @@ -122,13 +125,6 @@ export const AddProject = () => {
{...register('title')}
placeholder="Provide project title"
/>
<InputField
label="Project name"
size="large"
error={errors.name && String(errors.name?.message)}
{...register('name')}
placeholder="Provide project name"
/>
</Flex>
</Dialog.Body>
<Dialog.Footer>
Expand Down
21 changes: 21 additions & 0 deletions sdks/js/packages/core/react/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,27 @@ export const enrichBasePlan = (plan?: BasePlan): Plan | undefined => {
export const defaultFetch = (...fetchParams: Parameters<typeof fetch>) =>
fetch(...fetchParams);

export function generateHashFromString(
input: string,
hashLength = 6
): string {
if (!input || input.length === 0) {
throw new Error('Input string cannot be empty');
}
if (hashLength < 1 || hashLength > 12) {
throw new Error('Hash length must be between 1 and 12');
}

let hash = 5381;
for (let i = 0; i < input.length; i++) {
hash = (hash * 33) ^ input.charCodeAt(i);
}
return Math.abs(hash)
.toString(36)
.padStart(hashLength, '0')
.substring(0, hashLength);
}

export interface HttpErrorResponse extends Response {
data: unknown;
error: GooglerpcStatus;
Expand Down
Loading