From aa9ad5594c8cac7550a20c87e5b1a7e969259475 Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Wed, 8 Mar 2023 22:48:20 -0800 Subject: [PATCH] Link up teams UI. --- components/messages.js | 30 +++-- .../pages/settings/teams/TeamWebsites.js | 54 +++++++-- .../pages/settings/teams/TeamWebsitesTable.js | 105 ++++++++++++++++++ components/pages/settings/teams/TeamsList.js | 5 +- components/pages/settings/teams/TeamsTable.js | 64 ++++++----- .../settings/teams/WebsiteAddTeamForm.js | 62 +++++++++++ .../pages/settings/teams/WebsiteTags.js | 29 +++++ .../settings/teams/WebsiteTags.module.css | 11 ++ .../settings/websites/WebsiteSettings.js | 2 +- .../07_remove_user_id/migration.sql | 19 ++++ db/postgresql/schema.prisma | 11 +- lib/auth.ts | 29 ++++- pages/api/teamWebsites/[id].ts | 31 ++++++ pages/api/teams/[id]/websites.ts | 25 ++++- pages/api/teams/index.ts | 12 +- pages/api/teams/join.ts | 6 +- queries/admin/team.ts | 22 +--- queries/admin/teamWebsite.ts | 101 ++++++++++++++--- 18 files changed, 504 insertions(+), 114 deletions(-) create mode 100644 components/pages/settings/teams/TeamWebsitesTable.js create mode 100644 components/pages/settings/teams/WebsiteAddTeamForm.js create mode 100644 components/pages/settings/teams/WebsiteTags.js create mode 100644 components/pages/settings/teams/WebsiteTags.module.css create mode 100644 db/postgresql/migrations/07_remove_user_id/migration.sql create mode 100644 pages/api/teamWebsites/[id].ts diff --git a/components/messages.js b/components/messages.js index 77a26d27..60edcbfb 100644 --- a/components/messages.js +++ b/components/messages.js @@ -46,6 +46,7 @@ export const labels = defineMessages({ deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' }, reset: { id: 'label.reset', defaultMessage: 'Reset' }, addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' }, + addWebsites: { id: 'label.add-websites', defaultMessage: 'Add websites' }, changePassword: { id: 'label.change-password', defaultMessage: 'Change password' }, currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' }, newPassword: { id: 'label.new-password', defaultMessage: 'New password' }, @@ -98,8 +99,6 @@ export const labels = defineMessages({ sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, logs: { id: 'label.activity-log', defaultMessage: 'Activity log' }, - dismiss: { id: 'label.dismiss', defaultMessage: 'Dismiss' }, - poweredBy: { id: 'label.powered-by', defaultMessage: 'Powered by {name}' }, }); export const messages = defineMessages({ @@ -141,6 +140,10 @@ export const messages = defineMessages({ id: 'message.reset-website', defaultMessage: 'To reset this website, type {confirmation} in the box below to confirm.', }, + websitesShared: { + id: 'message.shared-website', + defaultMessage: 'Websites can be viewed by the entire team.', + }, invalidDomain: { id: 'message.invalid-domain', defaultMessage: 'Invalid domain. Do not include http/https.', @@ -158,6 +161,14 @@ export const messages = defineMessages({ id: 'messages.no-websites', defaultMessage: 'You do not have any websites configured.', }, + noTeamWebsites: { + id: 'messages.no-team-websites', + defaultMessage: 'This team does not have any websites.', + }, + websitesAreShared: { + id: 'messages.websites-are-shared', + defaultMessage: 'Websites can be viewed by anyone on the team.', + }, noMatchPassword: { id: 'message.no-match-password', defaultMessage: 'Passwords do not match.' }, goToSettings: { id: 'message.go-to-settings', @@ -179,17 +190,6 @@ export const messages = defineMessages({ id: 'message.event-log', defaultMessage: '{event} on {url}', }, - newVersionAvailable: { - id: 'new-version-available', - defaultMessage: 'A new version of Umami {version} is available!', - }, -}); - -export const devices = defineMessages({ - desktop: { id: 'metrics.device.desktop', defaultMessage: 'Desktop' }, - laptop: { id: 'metrics.device.laptop', defaultMessage: 'Laptop' }, - tablet: { id: 'metrics.device.tablet', defaultMessage: 'Tablet' }, - mobile: { id: 'metrics.device.mobile', defaultMessage: 'Mobile' }, }); export function getMessage(id, formatMessage) { @@ -197,7 +197,3 @@ export function getMessage(id, formatMessage) { return message ? formatMessage(message) : id; } - -export function getDeviceMessage(device) { - return devices[device] || labels.unknown; -} diff --git a/components/pages/settings/teams/TeamWebsites.js b/components/pages/settings/teams/TeamWebsites.js index 1a04f490..494a6eb0 100644 --- a/components/pages/settings/teams/TeamWebsites.js +++ b/components/pages/settings/teams/TeamWebsites.js @@ -1,13 +1,26 @@ -import { Loading } from 'react-basics'; -import { useIntl } from 'react-intl'; +import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; +import { labels, messages } from 'components/messages'; +import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable'; import useApi from 'hooks/useApi'; -import WebsitesTable from 'components/pages/settings/websites/WebsitesTable'; -import { messages } from 'components/messages'; +import { + ActionForm, + Button, + Icon, + Icons, + Loading, + Modal, + ModalTrigger, + Text, + useToast, +} from 'react-basics'; +import { useIntl } from 'react-intl'; +import WebsiteAddTeamForm from 'components/pages/settings/teams/WebsiteAddTeamForm'; export default function TeamWebsites({ teamId }) { + const { toast, showToast } = useToast(); const { formatMessage } = useIntl(); const { get, useQuery } = useApi(); - const { data, isLoading } = useQuery(['teams:websites', teamId], () => + const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () => get(`/teams/${teamId}/websites`), ); const hasData = data && data.length !== 0; @@ -16,10 +29,37 @@ export default function TeamWebsites({ teamId }) { return ; } + const handleSave = async () => { + await refetch(); + showToast({ message: formatMessage(messages.saved), variant: 'success' }); + }; + + const addButton = ( + + + + {close => } + + + ); + return (
- {hasData && } - {!hasData && formatMessage(messages.noData)} + {toast} + {hasData && ( + {addButton} + )} + {hasData && } + {!hasData && ( + + {addButton} + + )}
); } diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js new file mode 100644 index 00000000..b0d34221 --- /dev/null +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -0,0 +1,105 @@ +import Link from 'next/link'; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableCell, + TableColumn, + Button, + Text, + Icon, + Icons, + Flexbox, +} from 'react-basics'; +import { useIntl } from 'react-intl'; +import { labels } from 'components/messages'; +import useUser from 'hooks/useUser'; +import useApi from 'hooks/useApi'; + +export default function TeamWebsitesTable({ teamId, data = [], onSave }) { + const { formatMessage } = useIntl(); + const { user } = useUser(); + const { del, useMutation } = useApi(); + const { mutate } = useMutation(data => del(`/teamWebsites/${data.teamWebsiteId}`)); + + const columns = [ + { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, + { name: 'domain', label: formatMessage(labels.domain) }, + { name: 'action', label: ' ' }, + ]; + + const handleRemoveWebsite = teamWebsiteId => { + mutate( + { teamWebsiteId }, + { + onSuccess: async () => { + onSave(); + }, + }, + ); + }; + + return ( + + + {(column, index) => { + return ( + + {column.label} + + ); + }} + + + {(row, keys, rowIndex) => { + const { id: teamWebsiteId } = row; + const { id: websiteId, name, domain, userId } = row.website; + const { teamUser } = row.team; + const owner = teamUser[0]; + const canRemove = user.id === userId || user.id === owner.userId; + + row.name = name; + row.domain = domain; + + row.action = ( + + + + + + + {canRemove && ( + + )} + + ); + + return ( + + {(data, key, colIndex) => { + return ( + + + {data[key]} + + + ); + }} + + ); + }} + +
+ ); +} diff --git a/components/pages/settings/teams/TeamsList.js b/components/pages/settings/teams/TeamsList.js index b48e9971..0e6ef9c2 100644 --- a/components/pages/settings/teams/TeamsList.js +++ b/components/pages/settings/teams/TeamsList.js @@ -76,7 +76,10 @@ export default function TeamsList() { {hasData && } {!hasData && ( - {createButton} + + {joinButton} + {createButton} + )} diff --git a/components/pages/settings/teams/TeamsTable.js b/components/pages/settings/teams/TeamsTable.js index db99f9db..112a0992 100644 --- a/components/pages/settings/teams/TeamsTable.js +++ b/components/pages/settings/teams/TeamsTable.js @@ -1,26 +1,28 @@ +import { labels } from 'components/messages'; +import useUser from 'hooks/useUser'; +import { ROLES } from 'lib/constants'; import Link from 'next/link'; import { + Button, + Flexbox, + Icon, + Icons, + Modal, + ModalTrigger, Table, - TableHeader, TableBody, - TableRow, TableCell, TableColumn, - Button, - Icon, - Flexbox, - Icons, + TableHeader, + TableRow, Text, - ModalTrigger, - Modal, } from 'react-basics'; import { useIntl } from 'react-intl'; -import { labels } from 'components/messages'; -import { ROLES } from 'lib/constants'; import TeamDeleteForm from './TeamDeleteForm'; export default function TeamsTable({ data = [], onDelete }) { const { formatMessage } = useIntl(); + const { user } = useUser(); const columns = [ { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, @@ -42,10 +44,12 @@ export default function TeamsTable({ data = [], onDelete }) { {(row, keys, rowIndex) => { const { id } = row; + const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner); + const showDelete = user.id === owner?.userId; const rowData = { ...row, - owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username, + owner: owner?.user?.username, action: ( @@ -58,24 +62,26 @@ export default function TeamsTable({ data = [], onDelete }) { - - - - {close => ( - - )} - - + {showDelete && ( + + + + {close => ( + + )} + + + )} ), }; diff --git a/components/pages/settings/teams/WebsiteAddTeamForm.js b/components/pages/settings/teams/WebsiteAddTeamForm.js new file mode 100644 index 00000000..5d85e5de --- /dev/null +++ b/components/pages/settings/teams/WebsiteAddTeamForm.js @@ -0,0 +1,62 @@ +import { labels } from 'components/messages'; +import useApi from 'hooks/useApi'; +import { useRef, useState } from 'react'; +import { Button, Dropdown, Form, FormButtons, FormRow, Item, SubmitButton } from 'react-basics'; +import { useIntl } from 'react-intl'; +import WebsiteTags from './WebsiteTags'; + +export default function WebsiteAddTeamForm({ teamId, onSave, onClose }) { + const { formatMessage } = useIntl(); + const { get, post, useQuery, useMutation } = useApi(); + const { mutate, error } = useMutation(data => post(`/teams/${teamId}/websites`, data)); + const { data: websites } = useQuery(['websites'], () => get('/websites')); + const [newWebsites, setNewWebsites] = useState([]); + const formRef = useRef(); + + const handleSubmit = () => { + mutate( + { websiteIds: newWebsites }, + { + onSuccess: async () => { + onSave(); + onClose(); + }, + }, + ); + }; + + const handleAddWebsite = value => { + if (!newWebsites.some(a => a === value)) { + const nextValue = [...newWebsites]; + + nextValue.push(value); + + setNewWebsites(nextValue); + } + }; + + const handleRemoveWebsite = value => { + const newValue = newWebsites.filter(a => a !== value); + + setNewWebsites(newValue); + }; + + return ( + <> +
+ + + {({ id, name }) => {name}} + + + + + + {formatMessage(labels.addWebsites)} + + + + + + ); +} diff --git a/components/pages/settings/teams/WebsiteTags.js b/components/pages/settings/teams/WebsiteTags.js new file mode 100644 index 00000000..19179422 --- /dev/null +++ b/components/pages/settings/teams/WebsiteTags.js @@ -0,0 +1,29 @@ +import { Button, Icon, Icons, Text } from 'react-basics'; +import styles from './WebsiteTags.module.css'; + +export default function WebsiteTags({ items = [], websites = [], onClick }) { + if (websites.length === 0) { + return null; + } + + return ( +
+ {websites.map(websiteId => { + const website = items.find(a => a.id === websiteId); + + return ( +
+ +
+ ); + })} +
+ ); +} diff --git a/components/pages/settings/teams/WebsiteTags.module.css b/components/pages/settings/teams/WebsiteTags.module.css new file mode 100644 index 00000000..50ae60a0 --- /dev/null +++ b/components/pages/settings/teams/WebsiteTags.module.css @@ -0,0 +1,11 @@ +.filters { + display: flex; + justify-content: flex-start; + align-items: flex-start; +} + +.tag { + text-align: center; + margin-bottom: 10px; + margin-right: 20px; +} diff --git a/components/pages/settings/websites/WebsiteSettings.js b/components/pages/settings/websites/WebsiteSettings.js index 02693c72..273942ee 100644 --- a/components/pages/settings/websites/WebsiteSettings.js +++ b/components/pages/settings/websites/WebsiteSettings.js @@ -59,7 +59,7 @@ export default function WebsiteSettings({ websiteId }) { } > - +