mirror of
https://github.com/docmost/docmost.git
synced 2025-11-16 05:21:10 +10:00
Support I18n (#243)
* feat: support i18n * feat: wip support i18n * feat: complete space translation * feat: complete page translation * feat: update space translation * feat: update workspace translation * feat: update group translation * feat: update workspace translation * feat: update page translation * feat: update user translation * chore: update pnpm-lock * feat: add query translation * refactor: merge to single file * chore: remove necessary code * feat: save language to BE * fix: only load current language * feat: save language to locale column * fix: cleanups * add language menu to preferences page * new translations * translate editor * Translate editor placeholders * translate space selection component --------- Co-authored-by: Philip Okugbe <phil@docmost.com> Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
@ -5,6 +5,7 @@ import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.
|
||||
import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx";
|
||||
import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx";
|
||||
import { SpaceRole } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AddSpaceMemberModalProps {
|
||||
spaceId: string;
|
||||
@ -12,6 +13,7 @@ interface AddSpaceMemberModalProps {
|
||||
export default function AddSpaceMembersModal({
|
||||
spaceId,
|
||||
}: AddSpaceMemberModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [memberIds, setMemberIds] = useState<string[]>([]);
|
||||
const [role, setRole] = useState<string>(SpaceRole.WRITER);
|
||||
@ -48,8 +50,8 @@ export default function AddSpaceMembersModal({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Add space members</Button>
|
||||
<Modal opened={opened} onClose={close} title="Add space members">
|
||||
<Button onClick={open}>{t("Add space members")}</Button>
|
||||
<Modal opened={opened} onClose={close} title={t("Add space members")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
|
||||
<Stack>
|
||||
@ -57,13 +59,13 @@ export default function AddSpaceMembersModal({
|
||||
<SpaceMemberRole
|
||||
onSelect={handleRoleSelection}
|
||||
defaultRole={role}
|
||||
label="Select role"
|
||||
label={t("Select role")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button onClick={handleSubmit} type="submit">
|
||||
Add
|
||||
{t("Add")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
|
||||
@ -6,6 +6,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useCreateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
import { computeSpaceSlug } from "@/lib";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(2).max(50),
|
||||
@ -23,6 +24,7 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function CreateSpaceForm() {
|
||||
const { t } = useTranslation();
|
||||
const createSpaceMutation = useCreateSpaceMutation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -74,8 +76,8 @@ export function CreateSpaceForm() {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="name"
|
||||
label="Space name"
|
||||
placeholder="e.g Product Team"
|
||||
label={t("Space name")}
|
||||
placeholder={t("e.g Product Team")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
@ -83,16 +85,16 @@ export function CreateSpaceForm() {
|
||||
<TextInput
|
||||
withAsterisk
|
||||
id="slug"
|
||||
label="Space slug"
|
||||
placeholder="e.g product"
|
||||
label={t("Space slug")}
|
||||
placeholder={t("e.g product")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("slug")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Space description"
|
||||
placeholder="e.g Space for product team"
|
||||
label={t("Space description")}
|
||||
placeholder={t("e.g Space for product team")}
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={2}
|
||||
@ -102,7 +104,7 @@ export function CreateSpaceForm() {
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">{t("Create")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { Button, Divider, Modal } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { CreateSpaceForm } from "@/features/space/components/create-space-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function CreateSpaceModal() {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={open}>Create space</Button>
|
||||
<Button onClick={open}>{t("Create space")}</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Create space">
|
||||
<Modal opened={opened} onClose={close} title={t("Create space")}>
|
||||
<Divider size="xs" mb="xs" />
|
||||
<CreateSpaceForm />
|
||||
</Modal>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useForm, zodResolver } from "@mantine/form";
|
||||
import * as z from "zod";
|
||||
import { useUpdateSpaceMutation } from "@/features/space/queries/space-query.ts";
|
||||
import { ISpace } from "@/features/space/types/space.types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
@ -24,6 +25,7 @@ interface EditSpaceFormProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateSpaceMutation = useUpdateSpaceMutation();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -65,8 +67,8 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
<Stack>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="e.g Sales"
|
||||
label={t("Name")}
|
||||
placeholder={t("e.g Sales")}
|
||||
variant="filled"
|
||||
readOnly={readOnly}
|
||||
{...form.getInputProps("name")}
|
||||
@ -74,7 +76,7 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
|
||||
<TextInput
|
||||
id="slug"
|
||||
label="Slug"
|
||||
label={t("Slug")}
|
||||
variant="filled"
|
||||
readOnly={readOnly}
|
||||
{...form.getInputProps("slug")}
|
||||
@ -82,8 +84,8 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
|
||||
<Textarea
|
||||
id="description"
|
||||
label="Description"
|
||||
placeholder="e.g Space for sales team to collaborate"
|
||||
label={t("Description")}
|
||||
placeholder={t("e.g Space for sales team to collaborate")}
|
||||
variant="filled"
|
||||
readOnly={readOnly}
|
||||
autosize
|
||||
@ -96,7 +98,7 @@ export function EditSpaceForm({ space, readOnly }: EditSpaceFormProps) {
|
||||
{!readOnly && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" disabled={!form.isDirty()}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,7 @@ import { useSearchSuggestionsQuery } from "@/features/search/queries/search-quer
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface MultiMemberSelectProps {
|
||||
onChange: (value: string[]) => void;
|
||||
@ -30,6 +31,7 @@ const renderMultiSelectOption: MultiSelectProps["renderOption"] = ({
|
||||
);
|
||||
|
||||
export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
|
||||
@ -83,14 +85,14 @@ export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
const updatedUserGroups = mergeItemsIntoGroups(
|
||||
data,
|
||||
userItems,
|
||||
"Select a user",
|
||||
t("Select a user"),
|
||||
);
|
||||
|
||||
// Merge group items into groups
|
||||
const finalData = mergeItemsIntoGroups(
|
||||
updatedUserGroups,
|
||||
groupItems,
|
||||
"Select a group",
|
||||
t("Select a group"),
|
||||
);
|
||||
|
||||
setData(finalData);
|
||||
@ -103,8 +105,8 @@ export function MultiMemberSelect({ onChange }: MultiMemberSelectProps) {
|
||||
renderOption={renderMultiSelectOption}
|
||||
hidePickedOptions
|
||||
maxDropdownHeight={300}
|
||||
label="Add members"
|
||||
placeholder="Search for users and groups"
|
||||
label={t("Add members")}
|
||||
placeholder={t("Search for users and groups")}
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SpaceCaslAction,
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceSettingsModalProps {
|
||||
spaceId: string;
|
||||
@ -17,11 +18,12 @@ interface SpaceSettingsModalProps {
|
||||
}
|
||||
|
||||
export default function SpaceSettingsModal({
|
||||
spaceId,
|
||||
opened,
|
||||
onClose,
|
||||
}: SpaceSettingsModalProps) {
|
||||
const {data: space, isLoading} = useSpaceQuery(spaceId);
|
||||
spaceId,
|
||||
opened,
|
||||
onClose,
|
||||
}: SpaceSettingsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
|
||||
const spaceRules = space?.membership?.permissions;
|
||||
const spaceAbility = useSpaceAbility(spaceRules);
|
||||
@ -50,10 +52,10 @@ export default function SpaceSettingsModal({
|
||||
<Tabs defaultValue="members">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab fw={500} value="general">
|
||||
Settings
|
||||
{t("Settings")}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab fw={500} value="members">
|
||||
Members
|
||||
{t("Members")}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { Avatar, Group, Select, SelectProps, Text } from '@mantine/core';
|
||||
import { useGetSpacesQuery } from '@/features/space/queries/space-query.ts';
|
||||
import { ISpace } from '../../types/space.types';
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { Avatar, Group, Select, SelectProps, Text } from "@mantine/core";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { ISpace } from "../../types/space.types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceSelectProps {
|
||||
onChange: (value: string) => void;
|
||||
@ -10,7 +11,7 @@ interface SpaceSelectProps {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const renderSelectOption: SelectProps['renderOption'] = ({ option }) => (
|
||||
const renderSelectOption: SelectProps["renderOption"] = ({ option }) => (
|
||||
<Group gap="sm">
|
||||
<Avatar color="initials" variant="filled" name={option.label} size={20} />
|
||||
<div>
|
||||
@ -20,7 +21,8 @@ const renderSelectOption: SelectProps['renderOption'] = ({ option }) => (
|
||||
);
|
||||
|
||||
export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(searchValue, 500);
|
||||
const { data: spaces, isLoading } = useGetSpacesQuery({
|
||||
query: debouncedQuery,
|
||||
@ -41,7 +43,7 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
|
||||
const filteredSpaceData = spaceData.filter(
|
||||
(user) =>
|
||||
!data.find((existingUser) => existingUser.value === user.value)
|
||||
!data.find((existingUser) => existingUser.value === user.value),
|
||||
);
|
||||
setData((prevData) => [...prevData, ...filteredSpaceData]);
|
||||
}
|
||||
@ -53,14 +55,14 @@ export function SpaceSelect({ onChange, label, value }: SpaceSelectProps) {
|
||||
renderOption={renderSelectOption}
|
||||
maxDropdownHeight={300}
|
||||
//label={label || 'Select space'}
|
||||
placeholder="Search for spaces"
|
||||
placeholder={t("Search for spaces")}
|
||||
searchable
|
||||
searchValue={searchValue}
|
||||
onSearchChange={setSearchValue}
|
||||
clearable
|
||||
variant="filled"
|
||||
onChange={onChange}
|
||||
nothingFoundMessage="No space found"
|
||||
nothingFoundMessage={t("No space found")}
|
||||
limit={50}
|
||||
checkIconPosition="right"
|
||||
comboboxProps={{ width: 300, withinPortal: false }}
|
||||
|
||||
@ -35,10 +35,12 @@ import {
|
||||
SpaceCaslSubject,
|
||||
} from "@/features/space/permissions/permissions.type.ts";
|
||||
import PageImportModal from "@/features/page/components/page-import-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SwitchSpace } from "./switch-space";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
|
||||
export function SpaceSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const [tree] = useAtom(treeApiAtom);
|
||||
const location = useLocation();
|
||||
const [opened, { open: openSettings, close: closeSettings }] =
|
||||
@ -89,7 +91,7 @@ export function SpaceSidebar() {
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Overview</span>
|
||||
<span>{t("Overview")}</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
|
||||
@ -100,7 +102,7 @@ export function SpaceSidebar() {
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Search</span>
|
||||
<span>{t("Search")}</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
|
||||
@ -111,7 +113,7 @@ export function SpaceSidebar() {
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Space settings</span>
|
||||
<span>{t("Space settings")}</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
|
||||
@ -129,7 +131,7 @@ export function SpaceSidebar() {
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>New page</span>
|
||||
<span>{t("New page")}</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
@ -139,7 +141,7 @@ export function SpaceSidebar() {
|
||||
<div className={clsx(classes.section, classes.sectionPages)}>
|
||||
<Group className={classes.pagesHeader} justify="space-between">
|
||||
<Text size="xs" fw={500} c="dimmed">
|
||||
Pages
|
||||
{t("Pages")}
|
||||
</Text>
|
||||
|
||||
{spaceAbility.can(
|
||||
@ -149,12 +151,12 @@ export function SpaceSidebar() {
|
||||
<Group gap="xs">
|
||||
<SpaceMenu spaceId={space.id} onSpaceSettings={openSettings} />
|
||||
|
||||
<Tooltip label="Create page" withArrow position="right">
|
||||
<Tooltip label={t("Create page")} withArrow position="right">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={18}
|
||||
onClick={handleCreatePage}
|
||||
aria-label="Create page"
|
||||
aria-label={t("Create page")}
|
||||
>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
@ -191,6 +193,7 @@ interface SpaceMenuProps {
|
||||
onSpaceSettings: () => void;
|
||||
}
|
||||
function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const [importOpened, { open: openImportModal, close: closeImportModal }] =
|
||||
useDisclosure(false);
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
@ -201,11 +204,15 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
<Menu width={200} shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip
|
||||
label="Import pages & space settings"
|
||||
label={t("Import pages & space settings")}
|
||||
withArrow
|
||||
position="top"
|
||||
>
|
||||
<ActionIcon variant="default" size={18} aria-label="Space menu">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={18}
|
||||
aria-label={t("Space menu")}
|
||||
>
|
||||
<IconDots />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
@ -216,7 +223,7 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
onClick={openImportModal}
|
||||
leftSection={<IconArrowDown size={16} />}
|
||||
>
|
||||
Import pages
|
||||
{t("Import pages")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
@ -232,7 +239,7 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
onClick={onSpaceSettings}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
Space settings
|
||||
{t("Space settings")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -5,12 +5,14 @@ import { Button, Divider, Group, Text } from '@mantine/core';
|
||||
import DeleteSpaceModal from './delete-space-modal';
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import ExportModal from "@/components/common/export-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SpaceDetailsProps {
|
||||
spaceId: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: space, isLoading } = useSpaceQuery(spaceId);
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
@ -20,7 +22,7 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
{space && (
|
||||
<div>
|
||||
<Text my="md" fw={600}>
|
||||
Details
|
||||
{t("Details")}
|
||||
</Text>
|
||||
<EditSpaceForm space={space} readOnly={readOnly} />
|
||||
|
||||
@ -33,12 +35,12 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
<div>
|
||||
<Text size="md">Export space</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Export all pages and attachments in this space
|
||||
{t("Export all pages and attachments in this space.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button onClick={openExportModal}>
|
||||
Export
|
||||
{t("Export")}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@ -46,9 +48,9 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
|
||||
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Delete space</Text>
|
||||
<Text size="md">{t("Delete space")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Delete this space with all its pages and data.
|
||||
{t("Delete this space with all its pages and data.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
||||
@ -5,8 +5,10 @@ import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import classes from "./space-grid.module.css";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceGrid() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
|
||||
const cards = data?.items.map((space, index) => (
|
||||
@ -33,7 +35,7 @@ export default function SpaceGrid() {
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs" fw={700} mt="md">
|
||||
{formatMemberCount(space.memberCount)}
|
||||
{formatMemberCount(space.memberCount, t)}
|
||||
</Text>
|
||||
</Card>
|
||||
));
|
||||
@ -41,7 +43,7 @@ export default function SpaceGrid() {
|
||||
return (
|
||||
<>
|
||||
<Text fz="sm" fw={500} mb={"md"}>
|
||||
Spaces you belong to
|
||||
{t("Spaces you belong to")}
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||
|
||||
@ -3,8 +3,10 @@ import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceHomeTabs() {
|
||||
const { t } = useTranslation();
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
|
||||
@ -13,7 +15,7 @@ export default function SpaceHomeTabs() {
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
Recently updated
|
||||
{t("Recently updated")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import {Table, Group, Text, Avatar} from "@mantine/core";
|
||||
import React, {useState} from "react";
|
||||
import {useGetSpacesQuery} from "@/features/space/queries/space-query.ts";
|
||||
import { Table, Group, Text, Avatar } from "@mantine/core";
|
||||
import React, { useState } from "react";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import {useDisclosure} from "@mantine/hooks";
|
||||
import {formatMemberCount} from "@/lib";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SpaceList() {
|
||||
const {data, isLoading} = useGetSpacesQuery();
|
||||
const [opened, {open, close}] = useDisclosure(false);
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [selectedSpaceId, setSelectedSpaceId] = useState<string>(null);
|
||||
|
||||
const handleClick = (spaceId: string) => {
|
||||
@ -22,8 +24,8 @@ export default function SpaceList() {
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Space</Table.Th>
|
||||
<Table.Th>Members</Table.Th>
|
||||
<Table.Th>{t("Space")}</Table.Th>
|
||||
<Table.Th>{t("Members")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
@ -31,7 +33,7 @@ export default function SpaceList() {
|
||||
{data?.items.map((space, index) => (
|
||||
<Table.Tr
|
||||
key={index}
|
||||
style={{cursor: "pointer"}}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => handleClick(space.id)}
|
||||
>
|
||||
<Table.Td>
|
||||
@ -51,9 +53,10 @@ export default function SpaceList() {
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
|
||||
<Table.Td>
|
||||
<Text size="sm" style={{whiteSpace: 'nowrap'}}>{formatMemberCount(space.memberCount)}</Text>
|
||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
{formatMemberCount(space.memberCount, t)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
import {Group, Table, Text, Menu, ActionIcon} from "@mantine/core";
|
||||
import { Group, Table, Text, Menu, ActionIcon } from "@mantine/core";
|
||||
import React from "react";
|
||||
import {IconDots} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {CustomAvatar} from "@/components/ui/custom-avatar.tsx";
|
||||
import { IconDots } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import {
|
||||
useChangeSpaceMemberRoleMutation,
|
||||
useRemoveSpaceMemberMutation,
|
||||
useSpaceMembersQuery,
|
||||
} from "@/features/space/queries/space-query.ts";
|
||||
import {IconGroupCircle} from "@/components/icons/icon-people-circle.tsx";
|
||||
import {IRemoveSpaceMember} from "@/features/space/types/space.types.ts";
|
||||
import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx";
|
||||
import { IRemoveSpaceMember } from "@/features/space/types/space.types.ts";
|
||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||
import {
|
||||
getSpaceRoleLabel,
|
||||
spaceRoleData,
|
||||
} from "@/features/space/types/space-role-data.ts";
|
||||
import {formatMemberCount} from "@/lib";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MemberType = "user" | "group";
|
||||
|
||||
@ -25,10 +26,12 @@ interface SpaceMembersProps {
|
||||
}
|
||||
|
||||
export default function SpaceMembersList({
|
||||
spaceId,
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const {data, isLoading} = useSpaceMembersQuery(spaceId);
|
||||
spaceId,
|
||||
readOnly,
|
||||
}: SpaceMembersProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useSpaceMembersQuery(spaceId);
|
||||
|
||||
const removeSpaceMember = useRemoveSpaceMemberMutation();
|
||||
const changeSpaceMemberRoleMutation = useChangeSpaceMemberRoleMutation();
|
||||
|
||||
@ -79,16 +82,17 @@ export default function SpaceMembersList({
|
||||
|
||||
const openRemoveModal = (memberId: string, type: MemberType) =>
|
||||
modals.openConfirmModal({
|
||||
title: "Remove space member",
|
||||
title: t("Remove space member"),
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Are you sure you want to remove this user from the space? The user
|
||||
will lose all access to this space.
|
||||
{t(
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
centered: true,
|
||||
labels: {confirm: "Remove", cancel: "Cancel"},
|
||||
confirmProps: {color: "red"},
|
||||
labels: { confirm: t("Remove"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onRemove(memberId, type),
|
||||
});
|
||||
|
||||
@ -99,8 +103,8 @@ export default function SpaceMembersList({
|
||||
<Table verticalSpacing={8}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Member</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>{t("Member")}</Table.Th>
|
||||
<Table.Th>{t("Role")}</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
@ -117,7 +121,7 @@ export default function SpaceMembersList({
|
||||
/>
|
||||
)}
|
||||
|
||||
{member.type === "group" && <IconGroupCircle/>}
|
||||
{member.type === "group" && <IconGroupCircle />}
|
||||
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
@ -127,7 +131,7 @@ export default function SpaceMembersList({
|
||||
{member.type == "user" && member?.email}
|
||||
|
||||
{member.type == "group" &&
|
||||
`Group - ${formatMemberCount(member?.memberCount)}`}
|
||||
`${t("Group")} - ${formatMemberCount(member?.memberCount, t)}`}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
@ -161,7 +165,7 @@ export default function SpaceMembersList({
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" c="gray">
|
||||
<IconDots size={20} stroke={2}/>
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
@ -171,7 +175,7 @@ export default function SpaceMembersList({
|
||||
openRemoveModal(member.id, member.type)
|
||||
}
|
||||
>
|
||||
Remove space member
|
||||
{t("Remove space member")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
Reference in New Issue
Block a user