diff --git a/apps/admin/src/helpers/index.ts b/apps/admin/src/helpers/index.ts deleted file mode 100644 index 879bbb65..00000000 --- a/apps/admin/src/helpers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './gen-random-password'; diff --git a/apps/admin/src/modules/users/form/create/fields/user-create-auth-methods-form-field.tsx b/apps/admin/src/modules/users/form/create/fields/user-create-auth-methods-form-field.tsx index b167e313..f2876fbb 100644 --- a/apps/admin/src/modules/users/form/create/fields/user-create-auth-methods-form-field.tsx +++ b/apps/admin/src/modules/users/form/create/fields/user-create-auth-methods-form-field.tsx @@ -6,8 +6,8 @@ import { import type { SdkCreateUserAuthMethodsT } from '@llm/sdk'; +import { genRandomPassword } from '@llm/commons'; import { Checkbox, FormField, Input } from '@llm/ui'; -import { genRandomPassword } from '~/helpers'; import { useI18n } from '~/i18n'; type Props = ValidationErrorsListProps; diff --git a/apps/admin/src/modules/users/form/update/fields/index.ts b/apps/admin/src/modules/users/form/update/fields/index.ts index e4de1b74..4244f6db 100644 --- a/apps/admin/src/modules/users/form/update/fields/index.ts +++ b/apps/admin/src/modules/users/form/update/fields/index.ts @@ -1,2 +1,2 @@ -export * from './user-organization-info-form-field'; +export * from './user-organization-settings-form-field'; export * from './user-update-auth-methods-form-field'; diff --git a/apps/admin/src/modules/users/form/update/fields/user-organization-info-form-field.tsx b/apps/admin/src/modules/users/form/update/fields/user-organization-info-form-field.tsx deleted file mode 100644 index 1e0edf27..00000000 --- a/apps/admin/src/modules/users/form/update/fields/user-organization-info-form-field.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { SdkExtractUserT } from '@llm/sdk'; - -import { FormField } from '@llm/ui'; -import { useI18n } from '~/i18n'; -import { OrganizationsSearchSelect, UserOrganizationRoleSelect } from '~/modules/organizations'; - -type Props = { - user: SdkExtractUserT<'user'>; -}; - -export function UserOrganizationInfoField({ user }: Props) { - const t = useI18n().pack.modules.users.form.fields.organization; - - return ( - <> - - - - - - - - -
- - ); -} diff --git a/apps/admin/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx b/apps/admin/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx new file mode 100644 index 00000000..3e8c66de --- /dev/null +++ b/apps/admin/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx @@ -0,0 +1,50 @@ +import { + controlled, + useFormValidatorMessages, + type ValidationErrorsListProps, +} from '@under-control/forms'; + +import type { SdkTableRowWithIdNameT, SdkUpdateUserOrganizationInputT } from '@llm/sdk'; + +import { FormField } from '@llm/ui'; +import { useI18n } from '~/i18n'; +import { OrganizationsSearchSelect, UserOrganizationRoleSelect } from '~/modules/organizations'; + +type Props = ValidationErrorsListProps & { + organization: SdkTableRowWithIdNameT; +}; + +export const UserOrganizationSettingsFormField = controlled(( + { + organization, + errors, + control: { bind }, + }, +) => { + const t = useI18n().pack.modules.users.form.fields.organization; + const validation = useFormValidatorMessages({ errors }); + + return ( + <> + + + + + + + + +
+ + ); +}); diff --git a/apps/admin/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx b/apps/admin/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx index 11586142..69d3df2c 100644 --- a/apps/admin/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx +++ b/apps/admin/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx @@ -7,8 +7,8 @@ import { useMemo } from 'react'; import type { SdkUpdateUserAuthMethodsT } from '@llm/sdk'; +import { genRandomPassword } from '@llm/commons'; import { Checkbox, FormField, Input } from '@llm/ui'; -import { genRandomPassword } from '~/helpers'; import { useI18n } from '~/i18n'; type Props = ValidationErrorsListProps; diff --git a/apps/admin/src/modules/users/form/update/types.ts b/apps/admin/src/modules/users/form/update/types.ts new file mode 100644 index 00000000..50222338 --- /dev/null +++ b/apps/admin/src/modules/users/form/update/types.ts @@ -0,0 +1,8 @@ +import type { + SdkTableRowWithIdT, + SdkUpdateUserInputT, +} from '@llm/sdk'; + +export type UpdateUserFormValue = + SdkTableRowWithIdT & + SdkUpdateUserInputT; diff --git a/apps/admin/src/modules/users/form/update/use-user-update-form.tsx b/apps/admin/src/modules/users/form/update/use-user-update-form.tsx index 8155649e..98cd2e40 100644 --- a/apps/admin/src/modules/users/form/update/use-user-update-form.tsx +++ b/apps/admin/src/modules/users/form/update/use-user-update-form.tsx @@ -2,14 +2,16 @@ import { type FormHookAttrs, useForm } from '@under-control/forms'; import { flow } from 'fp-ts/lib/function'; import { runTask, tapTaskEither } from '@llm/commons'; -import { type SdkTableRowWithIdT, type SdkUpdateUserInputT, useSdkForLoggedIn } from '@llm/sdk'; +import { useSdkForLoggedIn } from '@llm/sdk'; import { usePredefinedFormValidators, useSaveTaskEitherNotification } from '@llm/ui'; +import type { UpdateUserFormValue } from './types'; + import { useUseAuthFormValidator } from '../shared'; type UpdateUserFormHookAttrs = & Omit< - FormHookAttrs, + FormHookAttrs, 'validation' | 'onSubmit' > & { @@ -23,9 +25,9 @@ export function useUserUpdateForm( }: UpdateUserFormHookAttrs, ) { const { sdks } = useSdkForLoggedIn(); - const { emailFormatValidator } = usePredefinedFormValidators(); + const { emailFormatValidator } = usePredefinedFormValidators(); const saveNotifications = useSaveTaskEitherNotification(); - const authValidator = useUseAuthFormValidator(); + const authValidator = useUseAuthFormValidator(); return useForm({ resetAfterSubmit: false, diff --git a/apps/admin/src/modules/users/form/update/user-update-form-modal.tsx b/apps/admin/src/modules/users/form/update/user-update-form-modal.tsx index 2ff321ec..52090f4f 100644 --- a/apps/admin/src/modules/users/form/update/user-update-form-modal.tsx +++ b/apps/admin/src/modules/users/form/update/user-update-form-modal.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; + import type { SdkUserT } from '@llm/sdk'; import { @@ -10,8 +12,10 @@ import { } from '@llm/ui'; import { useI18n } from '~/i18n'; +import type { UpdateUserFormValue } from './types'; + import { UserSharedFormFields } from '../shared'; -import { UserOrganizationInfoField, UserUpdateAuthMethodsFormField } from './fields'; +import { UserOrganizationSettingsFormField, UserUpdateAuthMethodsFormField } from './fields'; import { useUserUpdateForm } from './use-user-update-form'; export type UserUpdateFormModalProps = @@ -30,14 +34,32 @@ export function UserUpdateFormModal( }: UserUpdateFormModalProps, ) { const t = useI18n().pack.modules.users.form; - const { handleSubmitEvent, validator, submitState, bind } = useUserUpdateForm({ - defaultValue: { + + const defaultValue = useMemo(() => { + const attrs = { id: user.id, active: user.active, archiveProtection: user.archiveProtection, auth: user.auth, email: user.email, - }, + }; + + return ( + user.role === 'user' + ? { + ...attrs, + role: 'user', + organization: user.organization, + } + : { + ...attrs, + role: 'root', + } + ); + }, [user]); + + const { handleSubmitEvent, validator, submitState, bind } = useUserUpdateForm({ + defaultValue, onAfterSubmit, }); @@ -61,9 +83,12 @@ export function UserUpdateFormModal( )} > {user.role === 'user' && ( - + )} - = { + owner: 'Właściciel', + member: 'Użytkownik', +}; + export const I18N_PACK_EN = deepmerge(I18N_FORWARDED_EN_PACK, { navigation: { links: { @@ -389,6 +396,54 @@ export const I18N_PACK_EN = deepmerge(I18N_FORWARDED_EN_PACK, { remove: 'Remove from favorites', }, }, + organizations: { + userRoles: I18N_USER_ORGANIZATION_ROLES_EN, + }, + users: { + form: { + title: { + create: 'Create user', + edit: 'Edit user', + }, + fields: { + email: { + label: 'E-Mail', + placeholder: 'Enter e-mail address', + }, + flags: { + label: 'Flags', + }, + active: { + label: 'Active', + }, + organization: { + role: { + label: 'Role in organization', + }, + }, + auth: { + label: 'Authentication', + email: { + label: 'Email', + placeholder: 'Enter email address', + }, + password: { + label: 'Password', + placeholder: 'Enter password', + }, + resetPassword: { + label: 'Reset password', + }, + }, + }, + }, + row: { + authMethod: { + password: 'Password', + email: 'Email', + }, + }, + }, footer: { copyright: 'Open Source AI Platform', madeWith: 'Made with', diff --git a/apps/chat/src/i18n/packs/i18n-lang-pl.ts b/apps/chat/src/i18n/packs/i18n-lang-pl.ts index 09facbd4..18cbed75 100644 --- a/apps/chat/src/i18n/packs/i18n-lang-pl.ts +++ b/apps/chat/src/i18n/packs/i18n-lang-pl.ts @@ -1,9 +1,16 @@ import deepmerge from 'deepmerge'; +import type { SdkOrganizationUserRoleT } from '@llm/sdk'; + import { I18N_FORWARDED_PL_PACK } from '@llm/ui'; import type { I18nLangPack } from './i18n-packs'; +const I18N_USER_ORGANIZATION_ROLES_PL: Record = { + owner: 'Owner', + member: 'Member', +}; + export const I18N_PACK_PL: I18nLangPack = deepmerge(I18N_FORWARDED_PL_PACK, { navigation: { links: { @@ -391,6 +398,54 @@ export const I18N_PACK_PL: I18nLangPack = deepmerge(I18N_FORWARDED_PL_PACK, { remove: 'Usuń z ulubionych', }, }, + organizations: { + userRoles: I18N_USER_ORGANIZATION_ROLES_PL, + }, + users: { + form: { + title: { + create: 'Utwórz użytkownika', + edit: 'Edytuj użytkownika', + }, + fields: { + email: { + label: 'E-Mail', + placeholder: 'Wprowadź adres e-mail', + }, + flags: { + label: 'Flagi', + }, + active: { + label: 'Aktywny', + }, + organization: { + role: { + label: 'Rola w organizacji', + }, + }, + auth: { + label: 'Uwierzytelnianie', + email: { + label: 'Email', + placeholder: 'Wprowadź adres email', + }, + password: { + label: 'Hasło', + placeholder: 'Wprowadź hasło', + }, + resetPassword: { + label: 'Zresetuj hasło', + }, + }, + }, + }, + row: { + authMethod: { + email: 'E-Mail', + password: 'Hasło', + }, + }, + }, footer: { copyright: 'Platforma AI Open Source', madeWith: 'Stworzone z', diff --git a/apps/chat/src/modules/ai-models/controls/ai-models-search-select.tsx b/apps/chat/src/modules/ai-models/controls/ai-models-search-select.tsx index a3630465..061300cb 100644 --- a/apps/chat/src/modules/ai-models/controls/ai-models-search-select.tsx +++ b/apps/chat/src/modules/ai-models/controls/ai-models-search-select.tsx @@ -22,15 +22,12 @@ const AIModelsSearchAbstractSelect = createSdkAutocomplete< }); export function AIModelsSearchSelect({ filters, ...props }: ComponentProps) { - const { organization } = useWorkspaceOrganization(); + const { assignWorkspaceToFilters } = useWorkspaceOrganization(); return ( ); } diff --git a/apps/chat/src/modules/apps-categories/controls/apps-categories-search-select.tsx b/apps/chat/src/modules/apps-categories/controls/apps-categories-search-select.tsx index 15bbb730..1dc8a815 100644 --- a/apps/chat/src/modules/apps-categories/controls/apps-categories-search-select.tsx +++ b/apps/chat/src/modules/apps-categories/controls/apps-categories-search-select.tsx @@ -21,15 +21,12 @@ const AppsCategoriesAbstractSearchSelect = createSdkAutocomplete< }); export function AppsCategoriesSearchSelect({ filters, ...props }: ComponentProps) { - const { organization } = useWorkspaceOrganization(); + const { assignWorkspaceToFilters } = useWorkspaceOrganization(); return ( ); } diff --git a/apps/chat/src/modules/apps-categories/table/apps-categories-table-container.tsx b/apps/chat/src/modules/apps-categories/table/apps-categories-table-container.tsx index 11d48b75..e25c45bb 100644 --- a/apps/chat/src/modules/apps-categories/table/apps-categories-table-container.tsx +++ b/apps/chat/src/modules/apps-categories/table/apps-categories-table-container.tsx @@ -1,4 +1,4 @@ -import { pipe } from 'fp-ts/lib/function'; +import { flow, pipe } from 'fp-ts/lib/function'; import { tapTaskOption } from '@llm/commons'; import { useAsyncCallback } from '@llm/commons-front'; @@ -24,7 +24,7 @@ import { AppsCategoriesTableRow } from './apps-categories-table-row'; export function AppsCategoriesTableContainer() { const { pack } = useI18n(); - const { organization } = useWorkspaceOrganizationOrThrow(); + const { assignWorkspaceToFilters } = useWorkspaceOrganizationOrThrow(); const t = pack.table.columns; @@ -33,10 +33,7 @@ export function AppsCategoriesTableContainer() { storeDataInUrl: false, schema: SdKSearchAppsInputV, fallbackSearchParams: {}, - fetchResultsTask: filters => sdks.dashboard.appsCategories.search({ - ...filters, - organizationIds: [organization.id], - }), + fetchResultsTask: flow(assignWorkspaceToFilters, sdks.dashboard.appsCategories.search), }); const createModal = useAppCategoryCreateModal(); diff --git a/apps/chat/src/modules/apps/grid/app-card.tsx b/apps/chat/src/modules/apps/grid/app-card.tsx index f4b8ec41..cd335d1d 100644 --- a/apps/chat/src/modules/apps/grid/app-card.tsx +++ b/apps/chat/src/modules/apps/grid/app-card.tsx @@ -6,10 +6,6 @@ import { StarIcon, WandSparklesIcon } from 'lucide-react'; import { formatDate, runTask, tapTaskOption } from '@llm/commons'; import { type SdkAppT, useSdkForLoggedIn } from '@llm/sdk'; -import { useArchiveWithNotifications } from '@llm/ui'; -import { useI18n } from '~/i18n'; -import { useAppUpdateModal } from '~/modules/apps-creator'; -import { useCreateChatWithInitialApp } from '~/modules/chats/conversation/hooks'; import { CardActions, CardArchiveButton, @@ -19,7 +15,11 @@ import { CardFooter, CardOpenButton, CardTitle, -} from '~/modules/shared/card'; + useArchiveWithNotifications, +} from '@llm/ui'; +import { useI18n } from '~/i18n'; +import { useAppUpdateModal } from '~/modules/apps-creator'; +import { useCreateChatWithInitialApp } from '~/modules/chats/conversation/hooks'; import { useFavoriteApps } from '../favorite'; diff --git a/apps/chat/src/modules/apps/grid/apps-container.tsx b/apps/chat/src/modules/apps/grid/apps-container.tsx index efb9957d..76107a9d 100644 --- a/apps/chat/src/modules/apps/grid/apps-container.tsx +++ b/apps/chat/src/modules/apps/grid/apps-container.tsx @@ -1,6 +1,7 @@ import type { ControlHookResult } from '@under-control/forms'; import { clsx } from 'clsx'; +import { flow } from 'fp-ts/lib/function'; import { type ReactNode, useMemo } from 'react'; import { useLastNonNullValue, useUpdateEffect } from '@llm/commons-front'; @@ -33,7 +34,7 @@ type Props = { export function AppsContainer({ toolbar, itemPropsFn, columns = 3 }: Props) { const favorites = useFavoriteApps(); - const { organization } = useWorkspaceOrganizationOrThrow(); + const { assignWorkspaceToFilters } = useWorkspaceOrganizationOrThrow(); const { sdks } = useSdkForLoggedIn(); const { loading, pagination, result, silentReload } = useDebouncedPaginatedSearch({ @@ -46,10 +47,7 @@ export function AppsContainer({ toolbar, itemPropsFn, columns = 3 }: Props) { ids: [...favorites.ids], }, }, - fetchResultsTask: filters => sdks.dashboard.apps.search({ - ...filters, - organizationIds: [organization.id], - }), + fetchResultsTask: flow(assignWorkspaceToFilters, sdks.dashboard.apps.search), }); const categoriesTree = useLastNonNullValue(result?.aggs?.categories); diff --git a/apps/chat/src/modules/chats/grid/chat-card.tsx b/apps/chat/src/modules/chats/grid/chat-card.tsx index 151de986..9e2b925a 100644 --- a/apps/chat/src/modules/chats/grid/chat-card.tsx +++ b/apps/chat/src/modules/chats/grid/chat-card.tsx @@ -3,8 +3,8 @@ import { Link } from 'wouter'; import { formatDate } from '@llm/commons'; import { isSdkAIGeneratingString, type SdkSearchChatItemT } from '@llm/sdk'; +import { CardBase, CardDescription, CardFooter, CardTitle } from '@llm/ui'; import { useI18n } from '~/i18n'; -import { CardBase, CardDescription, CardFooter, CardTitle } from '~/modules/shared/card'; import { useSitemap } from '~/routes'; type ChatCardProps = { diff --git a/apps/chat/src/modules/organizations/controls/index.ts b/apps/chat/src/modules/organizations/controls/index.ts index cc519e86..f6aab647 100644 --- a/apps/chat/src/modules/organizations/controls/index.ts +++ b/apps/chat/src/modules/organizations/controls/index.ts @@ -1 +1,2 @@ export * from './organizations-search-select'; +export * from './user-organization-role-select'; diff --git a/apps/chat/src/modules/organizations/controls/user-organization-role-select.tsx b/apps/chat/src/modules/organizations/controls/user-organization-role-select.tsx new file mode 100644 index 00000000..fc350c44 --- /dev/null +++ b/apps/chat/src/modules/organizations/controls/user-organization-role-select.tsx @@ -0,0 +1,38 @@ +import { controlled, type OmitControlStateAttrs } from '@under-control/forms'; + +import type { SdkOrganizationUserRoleT } from '@llm/sdk'; + +import { findItemById } from '@llm/commons'; +import { Select, type SelectProps } from '@llm/ui'; +import { useI18n } from '~/i18n'; + +type Props = Omit, 'items'>; + +export const UserOrganizationRoleSelect = controlled(( + { + control: { value, setValue }, + ...props + }, +) => { + const { userRoles } = useI18n().pack.organizations; + const items = Object.entries(userRoles).map(([role, name]) => ({ + id: role, + name, + })); + + return ( + + + )} + + ); +}); diff --git a/apps/chat/src/modules/users/form/create/fields/user-organization-settings-form-field.tsx b/apps/chat/src/modules/users/form/create/fields/user-organization-settings-form-field.tsx new file mode 100644 index 00000000..48a9c526 --- /dev/null +++ b/apps/chat/src/modules/users/form/create/fields/user-organization-settings-form-field.tsx @@ -0,0 +1,37 @@ +import { + controlled, + useFormValidatorMessages, + type ValidationErrorsListProps, +} from '@under-control/forms'; + +import { FormField } from '@llm/ui'; +import { useI18n } from '~/i18n'; +import { UserOrganizationRoleSelect } from '~/modules/organizations'; + +import type { CreateUserOrganizationValue } from '../types'; + +type Props = ValidationErrorsListProps; + +export const UserOrganizationSettingsFormField = controlled(( + { + errors, + control: { bind }, + }, +) => { + const t = useI18n().pack.users.form.fields.organization; + const validation = useFormValidatorMessages({ errors }); + + return ( + <> + + + + +
+ + ); +}); diff --git a/apps/chat/src/modules/users/form/create/index.ts b/apps/chat/src/modules/users/form/create/index.ts new file mode 100644 index 00000000..1a9d9c89 --- /dev/null +++ b/apps/chat/src/modules/users/form/create/index.ts @@ -0,0 +1,3 @@ +export * from './use-user-create-form'; +export * from './use-user-create-modal'; +export * from './user-create-form-modal'; diff --git a/apps/chat/src/modules/users/form/create/types.ts b/apps/chat/src/modules/users/form/create/types.ts new file mode 100644 index 00000000..d1ee0eb5 --- /dev/null +++ b/apps/chat/src/modules/users/form/create/types.ts @@ -0,0 +1,12 @@ +import type { SdkCreateUserInputT, SdkCreateUserOrganizationInputT } from '@llm/sdk'; +import type { SelectItem } from '@llm/ui'; + +export type CreateUserOrganizationValue = SdkCreateUserOrganizationInputT & { + item: SelectItem; +}; + +export type CreateUserFormValue = + Extract | + Extract & { + organization: CreateUserOrganizationValue; + }; diff --git a/apps/chat/src/modules/users/form/create/use-user-create-form.tsx b/apps/chat/src/modules/users/form/create/use-user-create-form.tsx new file mode 100644 index 00000000..333f5816 --- /dev/null +++ b/apps/chat/src/modules/users/form/create/use-user-create-form.tsx @@ -0,0 +1,53 @@ +import { type FormHookAttrs, useForm } from '@under-control/forms'; +import { flow } from 'fp-ts/lib/function'; + +import { isObjectWithFakeID, runTask, tapTaskEither } from '@llm/commons'; +import { useSdkForLoggedIn } from '@llm/sdk'; +import { usePredefinedFormValidators, useSaveTaskEitherNotification } from '@llm/ui'; + +import type { CreateUserFormValue } from './types'; + +import { useUseAuthFormValidator } from '../shared'; + +type CreateUserFormHookAttrs = + & Omit< + FormHookAttrs, + 'validation' | 'onSubmit' + > + & { + onAfterSubmit?: VoidFunction; + }; + +export function useUserCreateForm( + { + onAfterSubmit, + ...props + }: CreateUserFormHookAttrs, +) { + const { sdks } = useSdkForLoggedIn(); + const { emailFormatValidator, requiredPathByPred } = usePredefinedFormValidators(); + const saveNotifications = useSaveTaskEitherNotification(); + const authValidator = useUseAuthFormValidator(); + + return useForm({ + resetAfterSubmit: false, + onSubmit: flow( + sdks.dashboard.users.create, + saveNotifications, + tapTaskEither(() => onAfterSubmit?.()), + runTask, + ), + validation: { + mode: ['blur', 'submit'], + validators: () => [ + emailFormatValidator('email'), + authValidator('auth'), + requiredPathByPred( + 'organization', + ({ globalValue, value }) => globalValue.role === 'user' && (!value?.item || isObjectWithFakeID(value.item)), + ), + ], + }, + ...props, + }); +} diff --git a/apps/chat/src/modules/users/form/create/use-user-create-modal.tsx b/apps/chat/src/modules/users/form/create/use-user-create-modal.tsx new file mode 100644 index 00000000..67c421bf --- /dev/null +++ b/apps/chat/src/modules/users/form/create/use-user-create-modal.tsx @@ -0,0 +1,32 @@ +import { useAnimatedModal } from '@llm/commons-front'; + +import type { CreateUserFormValue } from './types'; + +import { + UserCreateFormModal, + type UserCreateFormModalProps, +} from './user-create-form-modal'; + +type UserShowModalProps = + & Pick + & { + defaultValue: CreateUserFormValue; + }; + +export function useUserCreateModal() { + return useAnimatedModal({ + renderModalContent: ({ showProps, hiding, onAnimatedClose }) => ( + { + void onAnimatedClose(true); + showProps?.onAfterSubmit?.(); + }} + onClose={() => { + void onAnimatedClose(); + }} + /> + ), + }); +} diff --git a/apps/chat/src/modules/users/form/create/user-create-form-modal.tsx b/apps/chat/src/modules/users/form/create/user-create-form-modal.tsx new file mode 100644 index 00000000..73c9dcdd --- /dev/null +++ b/apps/chat/src/modules/users/form/create/user-create-form-modal.tsx @@ -0,0 +1,79 @@ +import { + CancelButton, + CreateButton, + FormErrorAlert, + Modal, + type ModalProps, + ModalTitle, +} from '@llm/ui'; +import { useI18n } from '~/i18n'; + +import type { + CreateUserFormValue, +} from './types'; + +import { UserSharedFormFields } from '../shared'; +import { UserCreateAuthMethodsFormField, UserOrganizationSettingsFormField } from './fields'; +import { useUserCreateForm } from './use-user-create-form'; + +export type UserCreateFormModalProps = + & Omit + & { + defaultValue: CreateUserFormValue; + onAfterSubmit?: VoidFunction; + }; + +export function UserCreateFormModal( + { + defaultValue, + onAfterSubmit, + onClose, + ...props + }: UserCreateFormModalProps, +) { + const t = useI18n().pack.users.form; + const { handleSubmitEvent, validator, submitState, bind, value } = useUserCreateForm({ + defaultValue, + onAfterSubmit, + }); + + return ( + + {t.title.create} + + )} + footer={( + <> + + + + )} + > + {value.role === 'user' && ( + + )} + + + + + + + + ); +} diff --git a/apps/chat/src/modules/users/form/index.ts b/apps/chat/src/modules/users/form/index.ts new file mode 100644 index 00000000..63cc03c7 --- /dev/null +++ b/apps/chat/src/modules/users/form/index.ts @@ -0,0 +1,3 @@ +export * from './create'; +export * from './shared'; +export * from './update'; diff --git a/apps/chat/src/modules/users/form/shared/index.ts b/apps/chat/src/modules/users/form/shared/index.ts new file mode 100644 index 00000000..bb5fe6eb --- /dev/null +++ b/apps/chat/src/modules/users/form/shared/index.ts @@ -0,0 +1,2 @@ +export * from './use-auth-form-validator'; +export * from './user-shared-form-fields'; diff --git a/apps/chat/src/modules/users/form/shared/use-auth-form-validator.ts b/apps/chat/src/modules/users/form/shared/use-auth-form-validator.ts new file mode 100644 index 00000000..3d498504 --- /dev/null +++ b/apps/chat/src/modules/users/form/shared/use-auth-form-validator.ts @@ -0,0 +1,43 @@ +import { + type ControlValue, + error, + type GetAllObjectPaths, + type GetAllObjectPathsEntries, + type PathValidator, +} from '@under-control/forms'; + +import { format } from '@llm/commons'; +import { SDK_MIN_PASSWORD_LENGTH, type SdkCreateUserAuthMethodsT } from '@llm/sdk'; +import { useI18n } from '~/i18n'; + +type AllAuthObjectPaths = Extract, { + type: { + password: { + enabled: boolean; + value?: string | null; + }; + }; +}>['path']; + +export function useUseAuthFormValidator() { + const t = useI18n().pack.validation.password; + + return

& AllAuthObjectPaths>(path: P): PathValidator => ({ + path, + fn: ({ value }) => { + const castedValue = value as SdkCreateUserAuthMethodsT; + + if ( + castedValue.password + && 'value' in castedValue.password + && castedValue.password.value.length < SDK_MIN_PASSWORD_LENGTH + ) { + return error( + format(t.mustBeLongerThan, { number: SDK_MIN_PASSWORD_LENGTH }), + null, + `${path}.password.value`, + ); + } + }, + }); +} diff --git a/apps/chat/src/modules/users/form/shared/user-shared-form-fields.tsx b/apps/chat/src/modules/users/form/shared/user-shared-form-fields.tsx new file mode 100644 index 00000000..1db22658 --- /dev/null +++ b/apps/chat/src/modules/users/form/shared/user-shared-form-fields.tsx @@ -0,0 +1,46 @@ +import { controlled, useFormValidatorMessages, type ValidationErrorsListProps } from '@under-control/forms'; + +import type { SdkUserT } from '@llm/sdk'; + +import { Checkbox, FormField, Input } from '@llm/ui'; +import { useI18n } from '~/i18n'; + +type Value = Pick; + +type Props = ValidationErrorsListProps; + +export const UserSharedFormFields = controlled(({ errors, control: { bind } }) => { + const t = useI18n().pack.users.form; + const validation = useFormValidatorMessages({ errors }); + + return ( + <> + + + + + + + {t.fields.active.label} + + + + ); +}); diff --git a/apps/chat/src/modules/users/form/update/fields/index.ts b/apps/chat/src/modules/users/form/update/fields/index.ts new file mode 100644 index 00000000..4244f6db --- /dev/null +++ b/apps/chat/src/modules/users/form/update/fields/index.ts @@ -0,0 +1,2 @@ +export * from './user-organization-settings-form-field'; +export * from './user-update-auth-methods-form-field'; diff --git a/apps/chat/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx b/apps/chat/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx new file mode 100644 index 00000000..2f9d9056 --- /dev/null +++ b/apps/chat/src/modules/users/form/update/fields/user-organization-settings-form-field.tsx @@ -0,0 +1,37 @@ +import { + controlled, + useFormValidatorMessages, + type ValidationErrorsListProps, +} from '@under-control/forms'; + +import type { SdkUpdateUserOrganizationInputT } from '@llm/sdk'; + +import { FormField } from '@llm/ui'; +import { useI18n } from '~/i18n'; +import { UserOrganizationRoleSelect } from '~/modules/organizations'; + +type Props = ValidationErrorsListProps; + +export const UserOrganizationSettingsFormField = controlled(( + { + errors, + control: { bind }, + }, +) => { + const t = useI18n().pack.users.form.fields.organization; + const validation = useFormValidatorMessages({ errors }); + + return ( + <> + + + + +


+ + ); +}); diff --git a/apps/chat/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx b/apps/chat/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx new file mode 100644 index 00000000..293ac5fd --- /dev/null +++ b/apps/chat/src/modules/users/form/update/fields/user-update-auth-methods-form-field.tsx @@ -0,0 +1,98 @@ +import { + controlled, + useFormValidatorMessages, + type ValidationErrorsListProps, +} from '@under-control/forms'; +import { useMemo } from 'react'; + +import type { SdkUpdateUserAuthMethodsT } from '@llm/sdk'; + +import { genRandomPassword } from '@llm/commons'; +import { Checkbox, FormField, Input } from '@llm/ui'; +import { useI18n } from '~/i18n'; + +type Props = ValidationErrorsListProps; + +export const UserUpdateAuthMethodsFormField = controlled(({ + errors, + control: { bind, value, setValue }, +}) => { + const t = useI18n().pack.users.form; + const validation = useFormValidatorMessages({ errors }); + + const hasInitiallyEnabledPassword = useMemo(() => value.password.enabled, []); + + const isResetPassword = 'value' in value.password; + const onResetPassword = (resetPassword: boolean) => { + setValue({ + merge: true, + value: { + password: { + enabled: true, + ...resetPassword ? { value: '' } : {}, + }, + }, + }); + }; + + return ( + <> + + + {t.fields.auth.email.label} + + + ({ + ...newGlobalValue, + password: { + enabled: newControlValue, + ...!hasInitiallyEnabledPassword && { + value: genRandomPassword(), + }, + }, + }), + })} + > + {t.fields.auth.password.label} + + + {hasInitiallyEnabledPassword && ( + + {t.fields.auth.resetPassword.label} + + )} + + + {(isResetPassword || (!hasInitiallyEnabledPassword && value.password.enabled)) && ( + + + + )} + + ); +}); diff --git a/apps/chat/src/modules/users/form/update/index.ts b/apps/chat/src/modules/users/form/update/index.ts new file mode 100644 index 00000000..1b0fc35c --- /dev/null +++ b/apps/chat/src/modules/users/form/update/index.ts @@ -0,0 +1,3 @@ +export * from './use-user-update-form'; +export * from './use-user-update-modal'; +export * from './user-update-form-modal'; diff --git a/apps/chat/src/modules/users/form/update/types.ts b/apps/chat/src/modules/users/form/update/types.ts new file mode 100644 index 00000000..50222338 --- /dev/null +++ b/apps/chat/src/modules/users/form/update/types.ts @@ -0,0 +1,8 @@ +import type { + SdkTableRowWithIdT, + SdkUpdateUserInputT, +} from '@llm/sdk'; + +export type UpdateUserFormValue = + SdkTableRowWithIdT & + SdkUpdateUserInputT; diff --git a/apps/chat/src/modules/users/form/update/use-user-update-form.tsx b/apps/chat/src/modules/users/form/update/use-user-update-form.tsx new file mode 100644 index 00000000..98cd2e40 --- /dev/null +++ b/apps/chat/src/modules/users/form/update/use-user-update-form.tsx @@ -0,0 +1,49 @@ +import { type FormHookAttrs, useForm } from '@under-control/forms'; +import { flow } from 'fp-ts/lib/function'; + +import { runTask, tapTaskEither } from '@llm/commons'; +import { useSdkForLoggedIn } from '@llm/sdk'; +import { usePredefinedFormValidators, useSaveTaskEitherNotification } from '@llm/ui'; + +import type { UpdateUserFormValue } from './types'; + +import { useUseAuthFormValidator } from '../shared'; + +type UpdateUserFormHookAttrs = + & Omit< + FormHookAttrs, + 'validation' | 'onSubmit' + > + & { + onAfterSubmit?: VoidFunction; + }; + +export function useUserUpdateForm( + { + onAfterSubmit, + ...props + }: UpdateUserFormHookAttrs, +) { + const { sdks } = useSdkForLoggedIn(); + const { emailFormatValidator } = usePredefinedFormValidators(); + const saveNotifications = useSaveTaskEitherNotification(); + const authValidator = useUseAuthFormValidator(); + + return useForm({ + resetAfterSubmit: false, + onSubmit: flow( + sdks.dashboard.users.update, + saveNotifications, + tapTaskEither(() => onAfterSubmit?.()), + runTask, + ), + validation: { + mode: ['blur', 'submit'], + validators: () => [ + emailFormatValidator('email'), + authValidator('auth'), + ], + }, + ...props, + }); +} diff --git a/apps/chat/src/modules/users/form/update/use-user-update-modal.tsx b/apps/chat/src/modules/users/form/update/use-user-update-modal.tsx new file mode 100644 index 00000000..9c1588ec --- /dev/null +++ b/apps/chat/src/modules/users/form/update/use-user-update-modal.tsx @@ -0,0 +1,32 @@ +import type { SdkUserT } from '@llm/sdk'; + +import { useAnimatedModal } from '@llm/commons-front'; + +import { + UserUpdateFormModal, + type UserUpdateFormModalProps, +} from './user-update-form-modal'; + +type UserShowModalProps = + & Pick + & { + user: SdkUserT; + }; + +export function useUserUpdateModal() { + return useAnimatedModal({ + renderModalContent: ({ showProps, hiding, onAnimatedClose }) => ( + { + void onAnimatedClose(true); + showProps?.onAfterSubmit?.(); + }} + onClose={() => { + void onAnimatedClose(); + }} + /> + ), + }); +} diff --git a/apps/chat/src/modules/users/form/update/user-update-form-modal.tsx b/apps/chat/src/modules/users/form/update/user-update-form-modal.tsx new file mode 100644 index 00000000..26d63ee9 --- /dev/null +++ b/apps/chat/src/modules/users/form/update/user-update-form-modal.tsx @@ -0,0 +1,105 @@ +import { useMemo } from 'react'; + +import type { SdkUserT } from '@llm/sdk'; + +import { + CancelButton, + FormErrorAlert, + Modal, + type ModalProps, + ModalTitle, + UpdateButton, +} from '@llm/ui'; +import { useI18n } from '~/i18n'; + +import type { UpdateUserFormValue } from './types'; + +import { UserSharedFormFields } from '../shared'; +import { UserOrganizationSettingsFormField, UserUpdateAuthMethodsFormField } from './fields'; +import { useUserUpdateForm } from './use-user-update-form'; + +export type UserUpdateFormModalProps = + & Omit + & { + user: SdkUserT; + onAfterSubmit?: VoidFunction; + }; + +export function UserUpdateFormModal( + { + user, + onAfterSubmit, + onClose, + ...props + }: UserUpdateFormModalProps, +) { + const t = useI18n().pack.users.form; + + const defaultValue = useMemo(() => { + const attrs = { + id: user.id, + active: user.active, + archiveProtection: user.archiveProtection, + auth: user.auth, + email: user.email, + }; + + return ( + user.role === 'user' + ? { + ...attrs, + role: 'user', + organization: user.organization, + } + : { + ...attrs, + role: 'root', + } + ); + }, [user]); + + const { handleSubmitEvent, validator, submitState, bind, value } = useUserUpdateForm({ + defaultValue, + onAfterSubmit, + }); + + return ( + + {t.title.edit} + + )} + footer={( + <> + + + + )} + > + {value.role === 'user' && ( + + )} + + + + + + + + ); +} diff --git a/apps/chat/src/modules/users/index.ts b/apps/chat/src/modules/users/index.ts new file mode 100644 index 00000000..b3eece8a --- /dev/null +++ b/apps/chat/src/modules/users/index.ts @@ -0,0 +1,2 @@ +export * from './form'; +export * from './table'; diff --git a/apps/chat/src/modules/users/table/index.ts b/apps/chat/src/modules/users/table/index.ts new file mode 100644 index 00000000..9e9a2a6e --- /dev/null +++ b/apps/chat/src/modules/users/table/index.ts @@ -0,0 +1,2 @@ +export * from './users-table-container'; +export * from './users-table-row'; diff --git a/apps/chat/src/modules/users/table/users-table-container.tsx b/apps/chat/src/modules/users/table/users-table-container.tsx new file mode 100644 index 00000000..83553612 --- /dev/null +++ b/apps/chat/src/modules/users/table/users-table-container.tsx @@ -0,0 +1,110 @@ +import { flow, pipe } from 'fp-ts/lib/function'; + +import { genRandomPassword, tapTaskOption } from '@llm/commons'; +import { useAsyncCallback } from '@llm/commons-front'; +import { SdKSearchUsersInputV, useSdkForLoggedIn } from '@llm/sdk'; +import { + ArchiveFilterTabs, + CreateButton, + PaginatedTable, + PaginationSearchToolbarItem, + PaginationToolbar, + ResetFiltersButton, + useDebouncedPaginatedSearch, +} from '@llm/ui'; +import { useI18n } from '~/i18n'; +import { useWorkspaceOrganizationOrThrow } from '~/modules/workspace'; + +import { useUserCreateModal } from '../form'; +import { UsersTableRow } from './users-table-row'; + +export function UsersTableContainer() { + const { pack } = useI18n(); + const t = pack.table.columns; + + const { sdks } = useSdkForLoggedIn(); + const { organization, assignWorkspaceToFilters } = useWorkspaceOrganizationOrThrow(); + + const { loading, pagination, result, reset, reload } = useDebouncedPaginatedSearch({ + schema: SdKSearchUsersInputV, + fallbackSearchParams: {}, + storeDataInUrl: false, + fetchResultsTask: flow(assignWorkspaceToFilters, sdks.dashboard.users.search), + }); + + const createModal = useUserCreateModal(); + const [onCreate, createState] = useAsyncCallback( + pipe( + createModal.showAsOptional({ + defaultValue: { + email: '', + role: 'user', + active: true, + archiveProtection: false, + organization: { + item: organization, + role: 'member', + }, + auth: { + email: { + enabled: true, + }, + password: { + enabled: true, + value: genRandomPassword(), + }, + }, + }, + }), + tapTaskOption(reset), + ), + ); + + return ( +
+ + + + + )} + > + ({ + ...newGlobalValue, + sort: newControlValue ? 'score:desc' : 'createdAt:asc', + }), + })} + /> + + + + + + {({ item }) => ( + + )} + +
+ ); +} diff --git a/apps/chat/src/modules/users/table/users-table-row.tsx b/apps/chat/src/modules/users/table/users-table-row.tsx new file mode 100644 index 00000000..4b00a1a8 --- /dev/null +++ b/apps/chat/src/modules/users/table/users-table-row.tsx @@ -0,0 +1,74 @@ +import { pipe } from 'fp-ts/lib/function'; +import { KeyRoundIcon, MailIcon } from 'lucide-react'; + +import { formatDate, tapTaskEither, tapTaskOption } from '@llm/commons'; +import { type SdkSearchUserItemT, useSdkForLoggedIn } from '@llm/sdk'; +import { ArchivedBadge, BooleanBadge, EllipsisCrudDropdownButton } from '@llm/ui'; +import { useI18n } from '~/i18n'; + +import { useUserUpdateModal } from '../form'; + +type Props = { + item: SdkSearchUserItemT; + onUpdated: VoidFunction; +}; + +export function UsersTableRow({ item, onUpdated }: Props) { + const t = useI18n().pack.users; + const { sdks } = useSdkForLoggedIn(); + const { auth } = item; + + const updateModal = useUserUpdateModal(); + + return ( + + {item.id} + {item.email} + + + + +
+ {auth.email?.enabled && ( + + + + )} + + {auth.password?.enabled && ( + + + + )} +
+ + + {formatDate(item.createdAt)} + {formatDate(item.updatedAt)} + + + + + ); +} diff --git a/apps/chat/src/modules/workspace/use-workspace-organization.tsx b/apps/chat/src/modules/workspace/use-workspace-organization.tsx index 376b48f2..c2108978 100644 --- a/apps/chat/src/modules/workspace/use-workspace-organization.tsx +++ b/apps/chat/src/modules/workspace/use-workspace-organization.tsx @@ -1,4 +1,4 @@ -import { useSdkForLoggedIn } from '@llm/sdk'; +import { type SdkTableRowIdT, useSdkForLoggedIn } from '@llm/sdk'; import { useWorkspace } from './workspace-context'; @@ -22,6 +22,12 @@ export function useWorkspaceOrganization() { ...obj, ...organization && { organization }, }), + assignWorkspaceToFilters: (obj: D) => ({ + ...obj, + ...organization && { + organizationIds: [organization.id], + }, + }), }; } @@ -36,7 +42,11 @@ export function useWorkspaceOrganizationOrThrow() { organization, assignWorkspaceOrganization: (obj: D) => ({ ...obj, - ...organization && { organization }, + organization, + }), + assignWorkspaceToFilters: (obj: D) => ({ + ...obj, + organizationIds: [organization.id], }), }; } diff --git a/apps/chat/src/routes/management/layout/management.layout.tsx b/apps/chat/src/routes/management/layout/management.layout.tsx index 8f3b73f3..9053ec4a 100644 --- a/apps/chat/src/routes/management/layout/management.layout.tsx +++ b/apps/chat/src/routes/management/layout/management.layout.tsx @@ -2,9 +2,9 @@ import type { PropsWithChildren } from 'react'; import { UserCircleIcon, UsersIcon } from 'lucide-react'; +import { SideLayout, SideNav, SideNavItem } from '@llm/ui'; import { useI18n } from '~/i18n'; import { LayoutHeader, PageWithNavigationLayout } from '~/layouts'; -import { SideLayout, SideNav, SideNavItem } from '~/modules'; import { RouteMetaTags, useSitemap } from '~/routes'; type Props = PropsWithChildren & { diff --git a/apps/chat/src/routes/management/pages/users-management.route.tsx b/apps/chat/src/routes/management/pages/users-management.route.tsx index 0cf2d432..cbee9428 100644 --- a/apps/chat/src/routes/management/pages/users-management.route.tsx +++ b/apps/chat/src/routes/management/pages/users-management.route.tsx @@ -1,4 +1,6 @@ +import { ContentCard } from '@llm/ui'; import { useI18n } from '~/i18n'; +import { UsersTableContainer } from '~/modules/users'; import { ManagementLayout } from '../layout'; @@ -7,7 +9,9 @@ export function UsersManagementRoute() { return ( - {t.title} + + + ); } diff --git a/apps/chat/src/routes/settings/settings.route.tsx b/apps/chat/src/routes/settings/settings.route.tsx index e0be0ef8..2664f206 100644 --- a/apps/chat/src/routes/settings/settings.route.tsx +++ b/apps/chat/src/routes/settings/settings.route.tsx @@ -1,9 +1,9 @@ import { BellRing, Bot, Database, Shield, Trash2, UserCircle } from 'lucide-react'; import { useState } from 'react'; +import { SideLayout, SideNav, SideNavItem } from '@llm/ui'; import { useI18n } from '~/i18n'; import { LayoutHeader, PageWithNavigationLayout } from '~/layouts'; -import { SideLayout, SideNav, SideNavItem } from '~/modules/shared'; import { RouteMetaTags } from '~/routes/shared'; type SectionId = 'account' | 'security' | 'notifications' | 'data'; diff --git a/apps/admin/src/helpers/gen-random-password.tsx b/packages/commons/src/helpers/gen-random-password.ts similarity index 90% rename from apps/admin/src/helpers/gen-random-password.tsx rename to packages/commons/src/helpers/gen-random-password.ts index 70ded1e7..c66cf940 100644 --- a/apps/admin/src/helpers/gen-random-password.tsx +++ b/packages/commons/src/helpers/gen-random-password.ts @@ -1,4 +1,4 @@ -import { genRandomBetweenInclusive } from '@llm/commons'; +import { genRandomBetweenInclusive } from './gen-random-between-inclusive'; const RANDOM_PASSWORD_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?'; diff --git a/packages/commons/src/helpers/index.ts b/packages/commons/src/helpers/index.ts index d1f7759a..9fbe7bf2 100644 --- a/packages/commons/src/helpers/index.ts +++ b/packages/commons/src/helpers/index.ts @@ -7,6 +7,7 @@ export * from './find-item-by-id'; export * from './find-item-index-by-id'; export * from './format'; export * from './gen-random-between-inclusive'; +export * from './gen-random-password'; export * from './get-first-obj-key-value'; export * from './invert'; export * from './is-nil'; diff --git a/packages/sdk/src/modules/dashboard/users/dto/sdk-update-user.dto.ts b/packages/sdk/src/modules/dashboard/users/dto/sdk-update-user.dto.ts index d3768fcd..c2bf7163 100644 --- a/packages/sdk/src/modules/dashboard/users/dto/sdk-update-user.dto.ts +++ b/packages/sdk/src/modules/dashboard/users/dto/sdk-update-user.dto.ts @@ -2,14 +2,34 @@ import { z } from 'zod'; import { SdkTableRowWithArchiveProtectionV, SdkTableRowWithIdV } from '~/shared'; +import { SdkOrganizationUserRoleV } from '../../organizations/dto/sdk-organization-user.dto'; import { SdkUpdateUserAuthMethodsV } from './auth'; +export const SdkUpdateUserOrganizationInputV = z.object({ + role: SdkOrganizationUserRoleV, +}); + +export type SdkUpdateUserOrganizationInputT = z.infer< + typeof SdkUpdateUserOrganizationInputV +>; + export const SdkUpdateUserInputV = z.object({ email: z.string(), active: z.boolean(), auth: SdkUpdateUserAuthMethodsV, }) - .merge(SdkTableRowWithArchiveProtectionV); + .merge(SdkTableRowWithArchiveProtectionV) + .and( + z.discriminatedUnion('role', [ + z.object({ + role: z.literal('root'), + }), + z.object({ + role: z.literal('user'), + organization: SdkUpdateUserOrganizationInputV, + }), + ]), + ); export type SdkUpdateUserInputT = z.infer; diff --git a/apps/chat/src/modules/shared/card/card-action-buttons.tsx b/packages/ui/src/components/card/card-action-buttons.tsx similarity index 92% rename from apps/chat/src/modules/shared/card/card-action-buttons.tsx rename to packages/ui/src/components/card/card-action-buttons.tsx index 69386c14..6a8caf69 100644 --- a/apps/chat/src/modules/shared/card/card-action-buttons.tsx +++ b/packages/ui/src/components/card/card-action-buttons.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import clsx from 'clsx'; import { PencilIcon, TrashIcon } from 'lucide-react'; -import { useI18n } from '~/i18n'; +import { useForwardedI18n } from '~/i18n'; type CardButtonProps = { icon?: ReactNode; @@ -43,7 +43,7 @@ export function CardActionButton({ } export function CardEditButton({ onClick }: { onClick: () => void; }) { - const t = useI18n().pack; + const t = useForwardedI18n().pack; return ( void; }) { } export function CardArchiveButton({ onClick, loading }: { onClick: () => void; loading?: boolean; }) { - const t = useI18n().pack; + const t = useForwardedI18n().pack; return ( (null); const wrapperRef = useRef(null); @@ -60,13 +60,13 @@ export function CardDescription({ children, limitHeight, className }: Props) { {isExpanded ? ( <> - {t.chat.actions.expand.less} + {t.buttons.expand.less} ) : ( <> - {t.chat.actions.expand.more} + {t.buttons.expand.more} )} diff --git a/apps/chat/src/modules/shared/card/card-footer.tsx b/packages/ui/src/components/card/card-footer.tsx similarity index 100% rename from apps/chat/src/modules/shared/card/card-footer.tsx rename to packages/ui/src/components/card/card-footer.tsx diff --git a/apps/chat/src/modules/shared/card/card-open-button.tsx b/packages/ui/src/components/card/card-open-button.tsx similarity index 92% rename from apps/chat/src/modules/shared/card/card-open-button.tsx rename to packages/ui/src/components/card/card-open-button.tsx index 5ba055a5..63b51b6b 100644 --- a/apps/chat/src/modules/shared/card/card-open-button.tsx +++ b/packages/ui/src/components/card/card-open-button.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { ExternalLinkIcon } from 'lucide-react'; import { Link } from 'wouter'; -import { useI18n } from '~/i18n'; +import { useForwardedI18n } from '~/i18n'; type Props = { href?: string; @@ -11,7 +11,7 @@ type Props = { }; export function CardOpenButton({ href, onClick, loading }: Props) { - const t = useI18n().pack; + const t = useForwardedI18n().pack; const className = clsx( 'uk-button uk-button-secondary uk-button-small', loading && 'uk-disabled opacity-50', diff --git a/apps/chat/src/modules/shared/card/card-title.tsx b/packages/ui/src/components/card/card-title.tsx similarity index 100% rename from apps/chat/src/modules/shared/card/card-title.tsx rename to packages/ui/src/components/card/card-title.tsx diff --git a/apps/chat/src/modules/shared/card/index.ts b/packages/ui/src/components/card/index.ts similarity index 100% rename from apps/chat/src/modules/shared/card/index.ts rename to packages/ui/src/components/card/index.ts diff --git a/packages/ui/src/components/content-card.tsx b/packages/ui/src/components/content-card.tsx new file mode 100644 index 00000000..e8e60581 --- /dev/null +++ b/packages/ui/src/components/content-card.tsx @@ -0,0 +1,25 @@ +import type { PropsWithChildren, ReactNode } from 'react'; + +type Props = PropsWithChildren & { + title: string; + toolbar?: ReactNode; +}; + +export function ContentCard({ title, toolbar, children }: Props) { + return ( +
+
+

{title}

+ {toolbar && ( +
+ {toolbar} +
+ )} +
+ +
+ {children} +
+
+ ); +} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index a248a57c..34b393d8 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,6 +1,8 @@ export * from './alert'; export * from './balloon'; +export * from './card'; export * from './collapsible-panel'; +export * from './content-card'; export * from './controls'; export * from './ellipsis-dropdown'; export * from './form'; @@ -9,6 +11,7 @@ export * from './modal'; export * from './notifications'; export * from './pagination'; export * from './predefined'; +export * from './side-layout'; export * from './skeleton'; export * from './spinner-container'; export * from './table'; diff --git a/apps/chat/src/modules/shared/side-layout/index.ts b/packages/ui/src/components/side-layout/index.ts similarity index 100% rename from apps/chat/src/modules/shared/side-layout/index.ts rename to packages/ui/src/components/side-layout/index.ts diff --git a/apps/chat/src/modules/shared/side-layout/side-layout.tsx b/packages/ui/src/components/side-layout/side-layout.tsx similarity index 87% rename from apps/chat/src/modules/shared/side-layout/side-layout.tsx rename to packages/ui/src/components/side-layout/side-layout.tsx index fa511bbc..83b1b320 100644 --- a/apps/chat/src/modules/shared/side-layout/side-layout.tsx +++ b/packages/ui/src/components/side-layout/side-layout.tsx @@ -10,7 +10,7 @@ export function SideLayout({ sidebar, children }: SideLayoutProps) { -
+
{children}
diff --git a/apps/chat/src/modules/shared/side-layout/side-nav-item.tsx b/packages/ui/src/components/side-layout/side-nav-item.tsx similarity index 100% rename from apps/chat/src/modules/shared/side-layout/side-nav-item.tsx rename to packages/ui/src/components/side-layout/side-nav-item.tsx diff --git a/apps/chat/src/modules/shared/side-layout/side-nav.tsx b/packages/ui/src/components/side-layout/side-nav.tsx similarity index 100% rename from apps/chat/src/modules/shared/side-layout/side-nav.tsx rename to packages/ui/src/components/side-layout/side-nav.tsx diff --git a/packages/ui/src/i18n/packs/i18n-forwarded-en-pack.tsx b/packages/ui/src/i18n/packs/i18n-forwarded-en-pack.tsx index 7526319c..b15d180f 100644 --- a/packages/ui/src/i18n/packs/i18n-forwarded-en-pack.tsx +++ b/packages/ui/src/i18n/packs/i18n-forwarded-en-pack.tsx @@ -45,6 +45,10 @@ export const I18N_FORWARDED_EN_PACK = { confirm: 'Confirm', resetFilters: 'Reset filters', download: 'Download', + expand: { + more: 'More', + less: 'Less', + }, }, errors: { tagged: I18N_SDK_ERRORS_EN, diff --git a/packages/ui/src/i18n/packs/i18n-forwarded-pl-pack.tsx b/packages/ui/src/i18n/packs/i18n-forwarded-pl-pack.tsx index f237f87e..6dbb3eb4 100644 --- a/packages/ui/src/i18n/packs/i18n-forwarded-pl-pack.tsx +++ b/packages/ui/src/i18n/packs/i18n-forwarded-pl-pack.tsx @@ -47,6 +47,10 @@ export const I18N_FORWARDED_PL_PACK: typeof I18N_FORWARDED_EN_PACK = { add: 'Dodaj', confirm: 'Potwierdź', download: 'Pobierz', + expand: { + more: 'Więcej', + less: 'Mniej', + }, }, errors: { tagged: I18N_SDK_ERRORS_PL,