Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
82 changes: 44 additions & 38 deletions Composer/packages/client/src/pages/publish/BotStatusList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,56 @@ import { ActionButton, IconButton } from 'office-ui-fabric-react/lib/Button';
import { SharedColors } from '@uifabric/fluent-theme';
import { FontSizes } from '@uifabric/styling';
import get from 'lodash/get';
import { ITextField, TextField } from 'office-ui-fabric-react/lib/TextField';
import { useCopyToClipboard } from '@bfc/ui-shared';
import { Callout } from 'office-ui-fabric-react/lib/Callout';

import { ApiStatus } from '../../utils/publishStatusPollingUpdater';

import { PublishStatusList } from './PublishStatusList';
import { detailList, listRoot, tableView } from './styles';
import { BotPublishHistory, BotStatus } from './type';

const copiedCalloutStyles = {
root: {
padding: '10px',
},
};

type SkillManifestUrlFieldProps = {
url: string;
};

const SkillManifestUrlField = ({ url }: SkillManifestUrlFieldProps) => {
const { isCopiedToClipboard, copyTextToClipboard, resetIsCopiedToClipboard } = useCopyToClipboard(url);

const calloutTarget = useRef<HTMLElement>();
return (
<Fragment>
<ActionButton
className="skill-manifest-copy-button"
title={url}
onClick={(e) => {
calloutTarget.current = e.target as HTMLElement;
copyTextToClipboard();
}}
>
{formatMessage('Copy Skill Manifest URL')}
</ActionButton>
{isCopiedToClipboard && (
<Callout
setInitialFocus
calloutMaxWidth={200}
styles={copiedCalloutStyles}
target={calloutTarget.current}
onDismiss={resetIsCopiedToClipboard}
>
{formatMessage('Skill manifest URL was copied to the clipboard')}
</Callout>
)}
</Fragment>
);
};

export type BotStatusListProps = {
botStatusList: BotStatus[];
botPublishHistoryList: BotPublishHistory;
Expand All @@ -49,25 +91,6 @@ export const BotStatusList: React.FC<BotStatusListProps> = ({
}) => {
const [expandedBotIds, setExpandedBotIds] = useState<Record<string, boolean>>({});
const [currentSort, setSort] = useState({ key: 'Bot', descending: true });
const [clipboardText, setClipboardText] = useState('');
const clipboardTextFieldRef = useRef<ITextField>(null);

const copyStringToClipboard = (value?: string) => {
try {
if (clipboardTextFieldRef.current) {
setClipboardText(value || '');
setTimeout(() => {
if (clipboardTextFieldRef.current) {
clipboardTextFieldRef.current.select();
document.execCommand('copy');
}
}, 10);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('Something went wrong when copying to the clipboard.', e, location);
}
};

const displayedItems: BotStatus[] = useMemo(() => {
if (currentSort.key !== 'Bot') return botStatusList;
Expand Down Expand Up @@ -273,18 +296,7 @@ export const BotStatusList: React.FC<BotStatusListProps> = ({
maxWidth: 134,
data: 'string',
onRender: (item: BotStatus) => {
return (
item?.skillManifestUrl && (
<ActionButton
title={item.skillManifestUrl}
onClick={() => {
copyStringToClipboard(item.skillManifestUrl);
}}
>
{formatMessage('Copy Skill Manifest URL')}
</ActionButton>
)
);
return item?.skillManifestUrl && <SkillManifestUrlField url={item.skillManifestUrl} />;
},
isPadded: true,
},
Expand Down Expand Up @@ -365,12 +377,6 @@ export const BotStatusList: React.FC<BotStatusListProps> = ({
onRenderRow={renderTableRow}
/>
</div>
<TextField
readOnly
componentRef={clipboardTextFieldRef}
styles={{ root: { display: 'none' } }}
value={clipboardText}
/>
</div>
);
};
4 changes: 4 additions & 0 deletions Composer/packages/lib/ui-shared/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export * from './useCopyToClipboard';
55 changes: 55 additions & 0 deletions Composer/packages/lib/ui-shared/src/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import * as React from 'react';

/**
* Creates a textarea at then end of the document body then
* uses that to copy the specified text to the cliboard.
*/

export const copyToClipboard = (text: string) => {
// Remember the current selection
const rangeCount = document.getSelection()?.rangeCount || 0;
const selected = rangeCount > 0 ? document.getSelection()?.getRangeAt(0) : false;

// Create an offscreen textarea and copy the text
const element = document.createElement('textarea');
element.value = text;
element.setAttribute('readonly', '');
element.style.position = 'absolute';
element.style.left = '-9999px';
document.body.appendChild(element);
element.select();
const success = document.execCommand('copy');
document.body.removeChild(element);

// Restore the previous selection
if (selected) {
document.getSelection()?.removeAllRanges();
document.getSelection()?.addRange(selected);
}

if (!success) {
throw new Error('There was a problem copying to the clipboard.');
}
};

/**
* Hook to copy text to the clipboard.
*/
export const useCopyToClipboard = (text: string) => {
const [isCopiedToClipboard, setIsCopiedToClipboard] = React.useState(false);

const copyTextToClipboard = React.useCallback(() => {
copyToClipboard(text);
setIsCopiedToClipboard(true);
}, [text]);

const resetIsCopiedToClipboard = React.useCallback(() => setIsCopiedToClipboard(false), []);

// When the text changes, reset copied.
React.useEffect(() => () => resetIsCopiedToClipboard(), [text]);

return { isCopiedToClipboard, copyTextToClipboard, resetIsCopiedToClipboard };
};
1 change: 1 addition & 0 deletions Composer/packages/lib/ui-shared/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
// Licensed under the MIT License.

export * from './components';
export * from './hooks';
export * from './styled';
export * from './constants';
3 changes: 3 additions & 0 deletions Composer/packages/server/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -3338,6 +3338,9 @@
"skill_manifest_endpoint_is_configured_improperly_e083731d": {
"message": "Skill manifest endpoint is configured improperly"
},
"skill_manifest_url_was_copied_to_the_clipboard_4cfad630": {
"message": "Skill manifest URL was copied to the clipboard"
},
"skillname_manifest_ef3d9fed": {
"message": "{ skillName } Manifest"
},
Expand Down