Hover cards for domains and people, and defaults in option tooltips
- A domain's name in any list opens a card on hover: whether it's taking mail, how many people it has, which of its key records (mail routing, SPF, DKIM, DMARC) are live in public DNS, and whether DNS, signing keys and certificates are automatic. - A person's shows their name, storage against their quota, role, groups and when they joined. The server keeps no last sign-in on the account, so the card doesn't claim one. - Cards load on open and are cached for a minute. - Option tooltips end with the option's default, and an option set away from its default gets a small "changed" mark.
This commit is contained in:
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useBufferedValue, useResetOnChange } from '@/hooks/useBufferedValue';
|
||||
import { HelpTip } from '@/help/HelpTip';
|
||||
import { fieldHelp } from '@/help/texts';
|
||||
import { describeDefault, differsFromDefault } from '@/help/defaults';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -88,6 +89,15 @@ export function FieldWidget(props: FieldWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
const { field, formField, value, onChange, readOnly, error, schema, sieveScriptName, helpScope } = props;
|
||||
const helpId = helpScope ? `${helpScope}.${formField.name}` : undefined;
|
||||
// INBUXA: the option's default, for its tooltip, and whether it has been changed.
|
||||
const defaultValue = helpScope ? schema.fields[helpScope]?.defaults?.[formField.name] : undefined;
|
||||
const defaultWords = describeDefault(field, defaultValue, schema, {
|
||||
on: t('field.on', 'On'),
|
||||
off: t('field.off', 'Off'),
|
||||
none: t('field.none', 'None'),
|
||||
});
|
||||
const defaultNote = defaultWords ? t('field.default', 'Default: {{value}}', { value: defaultWords }) : null;
|
||||
const changed = defaultWords !== null && differsFromDefault(value, defaultValue);
|
||||
const ft = field.type;
|
||||
const edition = useEffectiveEdition();
|
||||
|
||||
@@ -237,7 +247,15 @@ export function FieldWidget(props: FieldWidgetProps) {
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<HelpTip id={helpId} text={fieldHelp(helpId, field.description)} />
|
||||
<HelpTip id={helpId} text={fieldHelp(helpId, field.description)} footnote={defaultNote} />
|
||||
{changed && (
|
||||
<span
|
||||
className="rounded-full bg-primary/10 px-1.5 py-px text-[10px] font-medium text-primary"
|
||||
title={defaultNote ?? undefined}
|
||||
>
|
||||
{t('field.changed', 'changed')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{widget}
|
||||
{sieveScriptName !== undefined && ft.type === 'string' && (
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { EmptyState } from '@/components/common/EmptyState';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { HelpPanel } from '@/help/HelpPanel';
|
||||
import { ObjectHoverCard } from '@/features/hovercards/ObjectHoverCard';
|
||||
import { iconForView } from '@/lib/viewIcon';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -270,7 +271,11 @@ function renderCellValue(
|
||||
case 'objectId': {
|
||||
const id = String(value);
|
||||
const display = getDisplayName(ft.objectName, id);
|
||||
return display ?? id;
|
||||
return (
|
||||
<ObjectHoverCard objectName={ft.objectName} id={id}>
|
||||
{display ?? id}
|
||||
</ObjectHoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
case 'set': {
|
||||
@@ -1305,18 +1310,27 @@ export function DynamicList({ viewName }: DynamicListProps) {
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{list.columns.map((col) => (
|
||||
<td key={col.name} className="px-3 py-2">
|
||||
{renderCellValue(
|
||||
item[col.name],
|
||||
fields[col.name],
|
||||
col.name,
|
||||
schema!,
|
||||
resolved.obj.objectName,
|
||||
getDisplayName,
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{list.columns.map((col, colIndex) => {
|
||||
const cell = renderCellValue(
|
||||
item[col.name],
|
||||
fields[col.name],
|
||||
col.name,
|
||||
schema!,
|
||||
resolved.obj.objectName,
|
||||
getDisplayName,
|
||||
);
|
||||
return (
|
||||
<td key={col.name} className="px-3 py-2">
|
||||
{colIndex === 0 && item[col.name] != null ? (
|
||||
<ObjectHoverCard objectName={resolved.obj.objectName} id={itemId}>
|
||||
{cell}
|
||||
</ObjectHoverCard>
|
||||
) : (
|
||||
cell
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
{hasItemActions && <td className="px-3 py-2 text-right">{renderItemActions(item)}</td>}
|
||||
</tr>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user