Skip to content

Commit

Permalink
feat: Improve "Share" button with engine selection and clipboard supp…
Browse files Browse the repository at this point in the history
…ort, improve i18n (#188)

* feat: improve share button UX with toast notifications

* refactor: remove unused useCopy hook and related share state in EditorScreen

* feat: add selectedEngine state and integrate it into share functionality

* refactor: simplify enforcer logic in EditorScreen by removing redundant engine check

* feat: add localization support for sharing messages in useShareInfo hook

* feat: add translation for "Test completed successfully" in multiple languages
  • Loading branch information
HashCookie authored Jan 4, 2025
1 parent 666552a commit 0b0f8ae
Show file tree
Hide file tree
Showing 20 changed files with 197 additions and 171 deletions.
33 changes: 0 additions & 33 deletions app/components/editor/hooks/useCopy.tsx

This file was deleted.

36 changes: 31 additions & 5 deletions app/components/editor/hooks/useIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default function useIndex() {
const [share, setShare] = useState('');
const [triggerUpdate, setTriggerUpdate] = useState(0);
const [enforceContextData, setEnforceContextData] = useState(new Map(defaultEnforceContextData));
const [selectedEngine, setSelectedEngine] = useState('node');
const loadState = useRef<{
loadedHash?: string;
content?: ShareFormat;
Expand Down Expand Up @@ -74,6 +75,9 @@ export default function useIndex() {
setRequest(shared?.request ?? example[modelKind].request);
setCustomConfig(shared?.customConfig ?? defaultCustomConfig);
setEnforceContextData(new Map(Object.entries(JSON.parse(shared?.enforceContext || example[modelKind].enforceContext || defaultEnforceContext))));
if (shared?.selectedEngine) {
setSelectedEngine(shared.selectedEngine);
}
loadState.current.content = undefined;
}, [modelKind, triggerUpdate]);

Expand All @@ -88,9 +92,31 @@ export default function useIndex() {
}

return {
modelKind, setModelKind, modelText, setModelText, policy, setPolicy, request,
setRequest, echo, setEcho, requestResult, setRequestResult, customConfig, setCustomConfig, share, setShare,
enforceContextData, setEnforceContextData, setPolicyPersistent, setModelTextPersistent,
setCustomConfigPersistent, setRequestPersistent, setEnforceContextDataPersistent, handleShare,
} ;
modelKind,
setModelKind,
modelText,
setModelText,
policy,
setPolicy,
request,
setRequest,
echo,
setEcho,
requestResult,
setRequestResult,
customConfig,
setCustomConfig,
share,
setShare,
enforceContextData,
setEnforceContextData,
setPolicyPersistent,
setModelTextPersistent,
setCustomConfigPersistent,
setRequestPersistent,
setEnforceContextDataPersistent,
handleShare,
selectedEngine,
setSelectedEngine,
};
}
33 changes: 27 additions & 6 deletions app/components/editor/hooks/useShareInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import React, { useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useLang } from '@/app/context/LangContext';

interface ShareProps extends ShareFormat {
onResponse: (info: JSX.Element | string) => void;
Expand All @@ -26,6 +28,7 @@ export interface ShareFormat {
request?: string;
requestResult?: object;
enforceContext?: string;
selectedEngine?: string;
}

async function dpaste(content: string) {
Expand All @@ -39,13 +42,14 @@ async function dpaste(content: string) {

export default function useShareInfo() {
const [sharing, setSharing] = useState(false);
const { t } = useLang();

function shareInfo(props: ShareProps) {
if (sharing) return;
setSharing(true);
props.onResponse(<div className="text-orange-500">Sharing...</div>);

// Create an object that contains only non-null values
const loadingToast = toast.loading(t('Sharing'));

const shareContent: ShareFormat = {
...Object.entries(props).reduce((acc, [key, value]) => {
if (key !== 'onResponse' && value != null && value !== '') {
Expand All @@ -54,24 +58,41 @@ export default function useShareInfo() {
return acc;
}, {} as ShareFormat),
modelKind: props.modelKind,
selectedEngine: props.selectedEngine,
};

// Check if there are any non-null values to share
if (Object.keys(shareContent).length === 0) {
setSharing(false);
props.onResponse(<div className="text-red-500">No content to share</div>);
toast.error(t('No content to share'));
toast.dismiss(loadingToast);
return;
}

dpaste(JSON.stringify(shareContent))
.then((url: string) => {
setSharing(false);
const hash = url.split('/')[3];
const shareUrl = `${window.location.origin}${window.location.pathname}#${hash}`;

navigator.clipboard
.writeText(shareUrl)
.then(() => {
toast.success(t('Link copied to clipboard'), {
duration: 3000,
});
})
.catch(() => {
toast.error(t('Failed to copy link, please copy manually'));
});

props.onResponse(hash);
})
.catch((error) => {
setSharing(false);
props.onResponse(<div className="text-red-500">Error sharing content: {error.message}</div>);
toast.error(`${t('Share failed')}: ${error.message}`);
})
.finally(() => {
toast.dismiss(loadingToast);
});
}

Expand Down
154 changes: 59 additions & 95 deletions app/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { CasbinPolicySupport } from '@/app/components/editor/casbin-mode/casbin-
import { javascriptLanguage } from '@codemirror/lang-javascript';
import useRunTest from '@/app/components/editor/hooks/useRunTest';
import useShareInfo from '@/app/components/editor/hooks/useShareInfo';
import useCopy from '@/app/components/editor/hooks/useCopy';
import useSetupEnforceContext from '@/app/components/editor/hooks/useSetupEnforceContext';
import useIndex from '@/app/components/editor/hooks/useIndex';
import SidePanelChat from '@/app/components/SidePanelChat';
Expand All @@ -38,20 +37,19 @@ export const EditorScreen = () => {
requestResult,
setRequestResult,
customConfig,
share,
setShare,
enforceContextData,
setPolicyPersistent,
setModelTextPersistent,
setCustomConfigPersistent,
setRequestPersistent,
setEnforceContextDataPersistent,
handleShare,
selectedEngine,
setSelectedEngine,
} = useIndex();
const [open, setOpen] = useState(true);
const { enforcer } = useRunTest();
const { shareInfo } = useShareInfo();
const { copy } = useCopy();
const { setupEnforceContextData, setupHandleEnforceContextChange } = useSetupEnforceContext({
onChange: setEnforceContextDataPersistent,
data: enforceContextData,
Expand All @@ -70,44 +68,38 @@ export const EditorScreen = () => {
const { t, lang, theme, toggleTheme } = useLang();
const [isContentLoaded, setIsContentLoaded] = useState(false);
const casbinVersion = process.env.CASBIN_VERSION;
const [selectedEngine, setSelectedEngine] = useState('node');
const engineGithubLinks = {
node: `https://github.com/casbin/node-casbin/releases/tag/v${casbinVersion}`,
java: 'https://github.com/casbin/jcasbin/releases',
go: 'https://github.com/casbin/casbin/releases'
go: 'https://github.com/casbin/casbin/releases',
};

useEffect(() => {
if (modelKind && modelText) {
setIsContentLoaded(true);
if (selectedEngine === 'node') {
enforcer({
modelKind,
model: modelText,
policy,
customConfig,
request,
enforceContextData,
selectedEngine,
onResponse: (v) => {
if (isValidElement(v)) {
setEcho(v);
} else if (Array.isArray(v)) {
const formattedResults = v.map((res) => {
if (typeof res === 'object') {
const reasonString = Array.isArray(res.reason) && res.reason.length > 0 ? ` Reason: ${JSON.stringify(res.reason)}` : '';
return `${res.okEx}${reasonString}`;
}
return res;
});
setRequestResult(formattedResults.join('\n'));
}
},
});
} else {
setRequestResult('');
setEcho(null);
}
enforcer({
modelKind,
model: modelText,
policy,
customConfig,
request,
enforceContextData,
selectedEngine,
onResponse: (v) => {
if (isValidElement(v)) {
setEcho(v);
} else if (Array.isArray(v)) {
const formattedResults = v.map((res) => {
if (typeof res === 'object') {
const reasonString = Array.isArray(res.reason) && res.reason.length > 0 ? ` Reason: ${JSON.stringify(res.reason)}` : '';
return `${res.okEx}${reasonString}`;
}
return res;
});
setRequestResult(formattedResults.join('\n'));
}
},
});
}
}, [modelKind, modelText, policy, customConfig, request, enforceContextData, enforcer, setEcho, setRequestResult, selectedEngine]);
const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800');
Expand Down Expand Up @@ -141,7 +133,7 @@ export const EditorScreen = () => {
const result = formattedResults.join('\n');
setRequestResult(result);
if (result && !result.includes('error')) {
toast.success('Test completed successfully');
toast.success(t('Test completed successfully'));
}
}
},
Expand Down Expand Up @@ -275,21 +267,18 @@ export const EditorScreen = () => {
<select
className="bg-transparent border border-[#e13c3c] rounded px-2 py-1 text-[#e13c3c] focus:outline-none"
value={selectedEngine}
onChange={(e) => {return setSelectedEngine(e.target.value)}}
onChange={(e) => {
return setSelectedEngine(e.target.value);
}}
>
<option value="node">Node-Casbin(NodeJs) v{casbinVersion}</option>
<option value="java">jCasbin(Java)</option>
<option value="go">Casbin(Go)</option>
</select>
<a
href={engineGithubLinks[selectedEngine]}
target="_blank"
rel="noopener noreferrer"
className="text-[#e13c3c] hover:text-[#ff4d4d]"
>
<a href={engineGithubLinks[selectedEngine]} target="_blank" rel="noopener noreferrer" className="text-[#e13c3c] hover:text-[#ff4d4d]">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
{/* eslint-disable-next-line max-len */}
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
</a>
</div>
Expand Down Expand Up @@ -441,58 +430,33 @@ export const EditorScreen = () => {
>
{t('RUN THE TEST')}
</button>
{!share ? (
<button
className={clsx(
'rounded',
'px-2 py-1',
'border border-[#453d7d]',
'text-[#453d7a]',
'bg-[#efefef]',
'hover:bg-[#453d7d] hover:text-white',
'transition-colors duration-500',
)}
onClick={() => {
return shareInfo({
onResponse: (v) => {
return handleShare(v);
},
modelKind,
model: modelText,
policy,
customConfig,
request,
requestResult: Array.from(enforceContextData.entries()),
});
}}
>
{t('SHARE')}
</button>
) : (
<button
className={clsx(
'rounded',
'px-2 py-1',
'border border-[#453d7d]',
'text-[#453d7a]',
'bg-[#efefef]',
'hover:bg-[#453d7d] hover:text-white',
'transition-colors duration-500',
)}
onClick={() => {
return copy(
() => {
setShare('');
setEcho(<div className="text-green-500">{t('Copied')}</div>);
toast.success(t('Link copied to clipboard'));
},
`${window.location.origin + window.location.pathname}#${share}`,
);
}}
>
{t('COPY')}
</button>
)}
<button
className={clsx(
'rounded',
'px-2 py-1',
'border border-[#453d7d]',
'text-[#453d7a]',
'bg-[#efefef]',
'hover:bg-[#453d7d] hover:text-white',
'transition-colors duration-500',
)}
onClick={() => {
shareInfo({
onResponse: (v) => {
return handleShare(v);
},
modelKind,
model: modelText,
policy,
customConfig,
request,
requestResult: Array.from(enforceContextData.entries()),
selectedEngine,
});
}}
>
{t('SHARE')}
</button>
</div>

<div className="flex flex-row justify-between items-center w-full sm:w-auto sm:ml-auto mt-2 sm:mt-0">
Expand Down
Loading

0 comments on commit 0b0f8ae

Please sign in to comment.