fea47aa9f8
## Summary
- Re-implements `eslint-plugin-project-structure`'s folder structure
enforcement as a custom oxlint rule (`twenty/folder-structure`),
recovering functionality lost during the ESLint → Oxlint migration
- Validates `src/modules/` structure: kebab-case module folder names,
allowed subdirectories (hooks, utils, components, states, types,
graphql, etc.), hook file naming (`use{PascalCase}.(ts|tsx)`), util file
naming (`{camelCase}.(ts|tsx)`), and module nesting depth (max 4 levels)
- Enabled as `"warn"` in twenty-front with 403 pre-existing violations
to address incrementally
## What the rule checks
| Check | Example valid | Example invalid |
|-------|-------------|-----------------|
| Module names kebab-case | `object-record/` | `graphWidgetBarChart/` |
| Allowed subdirs only | `hooks/`, `components/`, `utils/` |
`random-stuff/` |
| Hook file naming | `useMyHook.ts` | `badName.ts` |
| Util file naming | `buildQuery.ts` | `build-query.ts` |
| Max nesting depth 4 | `a/b/c/d/hooks/` | `a/b/c/d/e/hooks/` |
| Utils kebab-case subfolders | `utils/cron-to-human/` |
`utils/camelCase/` |
## Pre-existing violations (403 total)
| Category | Count | Examples |
|----------|-------|---------|
| Non-kebab-case module names | 160 | `graphWidgetBarChart`,
`AIChatThreads` |
| Module depth > 4 | 215 |
`settings/roles/role-permissions/object-level-permissions/field-permissions`
|
| Util file naming | 22 | `.util.ts` suffix, kebab-case, PascalCase
filenames |
| Misc (hooks, tests) | 6 | Non-hook files in hooks/, folders in test
dirs |
211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
import { Section } from 'twenty-ui/layout';
|
|
import { H2Title, IconReload, IconTrash } from 'twenty-ui/display';
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
|
import { SettingsPath } from 'twenty-shared/types';
|
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
import { TextInput } from '@/ui/input/components/TextInput';
|
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
import { Button, ButtonGroup } from 'twenty-ui/input';
|
|
import { styled } from '@linaria/react';
|
|
import { SettingsDomainRecords } from '@/settings/domains/components/SettingsDomainRecords';
|
|
import { useCheckPublicDomainValidRecords } from '@/settings/domains/hooks/useCheckPublicDomainValidRecords';
|
|
import {
|
|
useCreatePublicDomainMutation,
|
|
useDeletePublicDomainMutation,
|
|
useFindManyPublicDomainsQuery,
|
|
} from '~/generated-metadata/graphql';
|
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
import { CheckPublicDomainValidRecordsEffect } from '@/settings/domains/components/CheckPublicDomainValidRecordsEffect';
|
|
import { selectedPublicDomainState } from '@/settings/domains/states/selectedPublicDomainState';
|
|
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
|
import { useState } from 'react';
|
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
|
import { getDomainValidationSchema } from '@/settings/domains/utils/getDomainValidationSchema';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
const StyledButtonGroupContainer = styled.div`
|
|
> * > :not(:first-of-type) > button {
|
|
border-left: none;
|
|
}
|
|
`;
|
|
|
|
const StyledButtonContainer = styled.div`
|
|
align-self: flex-start;
|
|
`;
|
|
|
|
const StyledDomainFormWrapper = styled.div`
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[2]};
|
|
`;
|
|
|
|
const StyledRecordsWrapper = styled.div`
|
|
margin-top: ${themeCssVariables.spacing[2]};
|
|
|
|
& > :not(:first-of-type) {
|
|
margin-top: ${themeCssVariables.spacing[4]};
|
|
}
|
|
`;
|
|
|
|
export const SettingPublicDomain = () => {
|
|
const [selectedPublicDomain, setSelectedPublicDomain] = useAtomState(
|
|
selectedPublicDomainState,
|
|
);
|
|
const { t } = useLingui();
|
|
const navigate = useNavigateSettings();
|
|
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
|
|
|
const [createPublicDomain, { loading }] = useCreatePublicDomainMutation();
|
|
|
|
const [newPublicDomain, setNewPublicDomain] = useState<string | undefined>(
|
|
selectedPublicDomain?.domain ?? '',
|
|
);
|
|
|
|
const [newPublicDomainError, setNewPublicDomainError] = useState<
|
|
string | undefined
|
|
>(undefined);
|
|
|
|
const { refetch: refetchPublicDomains } = useFindManyPublicDomainsQuery();
|
|
|
|
const [deletePublicDomain] = useDeletePublicDomainMutation();
|
|
|
|
const { isLoading, publicDomainRecords, checkPublicDomainRecords } =
|
|
useCheckPublicDomainValidRecords();
|
|
|
|
const onDelete = async () => {
|
|
if (!selectedPublicDomain) {
|
|
return;
|
|
}
|
|
|
|
await deletePublicDomain({
|
|
variables: { domain: selectedPublicDomain.domain },
|
|
onCompleted: () => {
|
|
enqueueSuccessSnackBar({
|
|
message: t`Public domain successfully deleted`,
|
|
});
|
|
navigate(SettingsPath.Domains);
|
|
refetchPublicDomains();
|
|
},
|
|
onError: (error) =>
|
|
enqueueErrorSnackBar({
|
|
apolloError: error,
|
|
}),
|
|
});
|
|
};
|
|
|
|
const validationSchema = getDomainValidationSchema(t);
|
|
|
|
const onCreate = async () => {
|
|
if (!isDefined(newPublicDomain)) {
|
|
return;
|
|
}
|
|
|
|
const result = validationSchema.safeParse(newPublicDomain);
|
|
|
|
if (!result.success) {
|
|
setNewPublicDomainError(result.error?.issues[0].message);
|
|
return;
|
|
}
|
|
|
|
setNewPublicDomainError(undefined);
|
|
|
|
await createPublicDomain({
|
|
variables: { domain: newPublicDomain },
|
|
onCompleted: (data) => {
|
|
setSelectedPublicDomain(data.createPublicDomain);
|
|
enqueueSuccessSnackBar({
|
|
message: t`Public domain created successfully`,
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
setNewPublicDomainError(error.message);
|
|
enqueueErrorSnackBar({
|
|
apolloError: error,
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<SubMenuTopBarContainer
|
|
title={t`Public domain`}
|
|
links={[
|
|
{
|
|
children: <Trans>Workspace</Trans>,
|
|
href: getSettingsPath(SettingsPath.Workspace),
|
|
},
|
|
{
|
|
children: <Trans>Domains</Trans>,
|
|
href: getSettingsPath(SettingsPath.Domains),
|
|
},
|
|
{ children: <Trans>Public Domain</Trans> },
|
|
]}
|
|
actionButton={
|
|
<SaveAndCancelButtons
|
|
onCancel={() => navigate(SettingsPath.Domains)}
|
|
isSaveDisabled={loading || isDefined(selectedPublicDomain)}
|
|
onSave={onCreate}
|
|
/>
|
|
}
|
|
>
|
|
<SettingsPageContainer>
|
|
<Section>
|
|
<H2Title
|
|
title={t`Public domain`}
|
|
description={t`Set the name of your public domain and configure your DNS records.`}
|
|
/>
|
|
{isDefined(selectedPublicDomain) && (
|
|
<CheckPublicDomainValidRecordsEffect
|
|
publicDomain={selectedPublicDomain}
|
|
/>
|
|
)}
|
|
<StyledDomainFormWrapper>
|
|
<TextInput
|
|
value={newPublicDomain}
|
|
onChange={setNewPublicDomain}
|
|
error={newPublicDomainError}
|
|
type="text"
|
|
disabled={isDefined(selectedPublicDomain)}
|
|
placeholder="crm.yourPublicDomain.com"
|
|
fullWidth
|
|
/>
|
|
{isDefined(selectedPublicDomain) && (
|
|
<StyledButtonGroupContainer>
|
|
<ButtonGroup>
|
|
<StyledButtonContainer>
|
|
<Button
|
|
isLoading={isLoading}
|
|
Icon={IconReload}
|
|
title={t`Reload`}
|
|
variant="primary"
|
|
onClick={() =>
|
|
checkPublicDomainRecords(selectedPublicDomain.domain)
|
|
}
|
|
type="button"
|
|
/>
|
|
</StyledButtonContainer>
|
|
<StyledButtonContainer>
|
|
<Button
|
|
Icon={IconTrash}
|
|
variant="primary"
|
|
onClick={onDelete}
|
|
/>
|
|
</StyledButtonContainer>
|
|
</ButtonGroup>
|
|
</StyledButtonGroupContainer>
|
|
)}
|
|
</StyledDomainFormWrapper>
|
|
{isDefined(selectedPublicDomain) && publicDomainRecords?.domain && (
|
|
<StyledRecordsWrapper>
|
|
{isDefined(publicDomainRecords.records) && (
|
|
<SettingsDomainRecords records={publicDomainRecords.records} />
|
|
)}
|
|
</StyledRecordsWrapper>
|
|
)}
|
|
</Section>
|
|
</SettingsPageContainer>
|
|
</SubMenuTopBarContainer>
|
|
);
|
|
};
|