mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-18 21:19:44 +00:00
chore: get shortcut by name and workspace name
This commit is contained in:
parent
ee801af742
commit
5a79304153
@ -5,6 +5,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/boojack/corgi/common"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
)
|
)
|
||||||
@ -21,10 +22,16 @@ func getFileSystem(path string) http.FileSystem {
|
|||||||
return http.FS(fs)
|
return http.FS(fs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func skipper(c echo.Context) bool {
|
||||||
|
path := c.Path()
|
||||||
|
return common.HasPrefixes(path, "/api")
|
||||||
|
}
|
||||||
|
|
||||||
func embedFrontend(e *echo.Echo) {
|
func embedFrontend(e *echo.Echo) {
|
||||||
// Use echo static middleware to serve the built dist folder
|
// Use echo static middleware to serve the built dist folder
|
||||||
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
|
// refer: https://github.com/labstack/echo/blob/master/middleware/static.go
|
||||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||||
|
Skipper: skipper,
|
||||||
HTML5: true,
|
HTML5: true,
|
||||||
Filesystem: getFileSystem("dist"),
|
Filesystem: getFileSystem("dist"),
|
||||||
}))
|
}))
|
||||||
@ -37,6 +44,7 @@ func embedFrontend(e *echo.Echo) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||||
|
Skipper: skipper,
|
||||||
HTML5: true,
|
HTML5: true,
|
||||||
Filesystem: getFileSystem("dist/assets"),
|
Filesystem: getFileSystem("dist/assets"),
|
||||||
}))
|
}))
|
||||||
|
@ -90,12 +90,68 @@ func (s *Server) registerWorkspaceRoutes(g *echo.Group) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
g.GET("/workspace/:workspaceName/shortcut/:shortcutName", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceName := c.Param("workspaceName")
|
||||||
|
shortcutName := c.Param("shortcutName")
|
||||||
|
if workspaceName == "" || shortcutName == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Missing workspace name or shortcut name")
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace, err := s.Store.FindWorkspace(ctx, &api.WorkspaceFind{
|
||||||
|
Name: &workspaceName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if common.ErrorCode(err) == common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("workspace not found by name %s", workspaceName))
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to find workspace with name %s", workspaceName)).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{
|
||||||
|
WorkspaceID: &workspace.ID,
|
||||||
|
UserID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if common.ErrorCode(err) == common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "workspace user not found")
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if workspaceUser == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "not workspace user")
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{
|
||||||
|
WorkspaceID: &workspace.ID,
|
||||||
|
Name: &shortcutName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if common.ErrorCode(err) == common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("shortcut not found by name %s", shortcutName))
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("failed to find shortcut with name %s", shortcutName)).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(shortcut)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode shortcut response").SetInternal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
g.PATCH("/workspace/:id", func(c echo.Context) error {
|
g.PATCH("/workspace/:id", func(c echo.Context) error {
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
}
|
}
|
||||||
|
|
||||||
workspaceID, err := strconv.Atoi(c.Param("id"))
|
workspaceID, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.1",
|
"@reduxjs/toolkit": "^1.8.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
"copy-to-clipboard": "^3.3.2",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
|
@ -7,6 +7,7 @@ import Auth from "./pages/Auth";
|
|||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import WorkspaceDetail from "./pages/WorkspaceDetail";
|
import WorkspaceDetail from "./pages/WorkspaceDetail";
|
||||||
import UserDetail from "./pages/UserDetail";
|
import UserDetail from "./pages/UserDetail";
|
||||||
|
import ShortcutRedirector from "./pages/ShortcutRedirector";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -33,6 +34,7 @@ function App() {
|
|||||||
<Route path="/auth" element={<Auth />} />
|
<Route path="/auth" element={<Auth />} />
|
||||||
<Route path="/user/:userId" element={<UserDetail />} />
|
<Route path="/user/:userId" element={<UserDetail />} />
|
||||||
<Route path="/workspace/:workspaceId" element={<WorkspaceDetail />} />
|
<Route path="/workspace/:workspaceId" element={<WorkspaceDetail />} />
|
||||||
|
<Route path="/:workspaceName/go/:shortcutName" element={<ShortcutRedirector />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Only>
|
</Only>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { shortcutService } from "../services";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { shortcutService, workspaceService } from "../services";
|
||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
import showCreateShortcutDialog from "./CreateShortcutDialog";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
@ -11,6 +12,11 @@ interface Props {
|
|||||||
const ShortcutListView: React.FC<Props> = (props: Props) => {
|
const ShortcutListView: React.FC<Props> = (props: Props) => {
|
||||||
const { workspaceId, shortcutList } = props;
|
const { workspaceId, shortcutList } = props;
|
||||||
|
|
||||||
|
const handleCopyButtonClick = (shortcut: Shortcut) => {
|
||||||
|
const workspace = workspaceService.getWorkspaceById(workspaceId);
|
||||||
|
copy(`${location.host}/${workspace?.name}/go/${shortcut.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => {
|
const handleDeleteShortcutButtonClick = (shortcut: Shortcut) => {
|
||||||
shortcutService.deleteShortcutById(shortcut.id);
|
shortcutService.deleteShortcutById(shortcut.id);
|
||||||
};
|
};
|
||||||
@ -20,33 +26,44 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
|||||||
{shortcutList.map((shortcut) => {
|
{shortcutList.map((shortcut) => {
|
||||||
return (
|
return (
|
||||||
<div key={shortcut.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
|
<div key={shortcut.id} className="w-full flex flex-row justify-between items-start border px-6 py-4 mb-3 rounded-lg">
|
||||||
<div className="flex flex-col justify-start items-start">
|
<div className="flex flex-row justify-start items-center mr-4">
|
||||||
<span className="text-lg font-medium">{shortcut.name}</span>
|
<span className="font-medium">{shortcut.name}</span>
|
||||||
<a className="text-base text-gray-600 flex flex-row cursor-pointer hover:underline" target="blank" href={shortcut.link}>
|
<span className="text-gray-500 ml-4">{shortcut.description}</span>
|
||||||
{shortcut.link} <Icon.ExternalLink className="w-4 h-auto ml-1" />
|
</div>
|
||||||
</a>
|
<div className="flex flex-row justify-end items-center">
|
||||||
|
<span
|
||||||
|
className="cursor-pointer mr-4 hover:opacity-80"
|
||||||
|
onClick={() => {
|
||||||
|
handleCopyButtonClick(shortcut);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon.Copy className="w-5 h-auto" />
|
||||||
|
</span>
|
||||||
|
<a className="cursor-pointer mr-4 hover:opacity-80" target="blank" href={shortcut.link}>
|
||||||
|
<Icon.ExternalLink className="w-5 h-auto" />
|
||||||
|
</a>
|
||||||
|
<Dropdown
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="w-full px-2 leading-8 cursor-pointer rounded hover:bg-gray-100"
|
||||||
|
onClick={() => showCreateShortcutDialog(workspaceId, shortcut.id)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="w-full px-2 leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteShortcutButtonClick(shortcut);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actionsClassName="!w-24"
|
||||||
|
></Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className="w-full px-2 leading-8 cursor-pointer rounded hover:bg-gray-100"
|
|
||||||
onClick={() => showCreateShortcutDialog(workspaceId, shortcut.id)}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="w-full px-2 leading-8 cursor-pointer rounded text-red-600 hover:bg-gray-100"
|
|
||||||
onClick={() => {
|
|
||||||
handleDeleteShortcutButtonClick(shortcut);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
actionsClassName="!w-24"
|
|
||||||
></Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -87,6 +87,10 @@ export function getShortcutList(shortcutFind?: ShortcutFind) {
|
|||||||
return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut?${queryList.join("&")}`);
|
return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut?${queryList.join("&")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getShortcutWithNameAndWorkspaceName(workspaceName: string, shortcutName: string) {
|
||||||
|
return axios.get<ResponseObject<Shortcut>>(`/api/workspace/${workspaceName}/shortcut/${shortcutName}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function createShortcut(shortcutCreate: ShortcutCreate) {
|
export function createShortcut(shortcutCreate: ShortcutCreate) {
|
||||||
return axios.post<ResponseObject<Shortcut>>("/api/shortcut", shortcutCreate);
|
return axios.post<ResponseObject<Shortcut>>("/api/shortcut", shortcutCreate);
|
||||||
}
|
}
|
||||||
|
@ -44,15 +44,3 @@ export function throttle(fn: FunctionType, delay: number) {
|
|||||||
}, delay);
|
}, delay);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyTextToClipboard(text: string) {
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.warn("Copy to clipboard failed.", error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Copy to clipboard failed, methods not supports.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
43
web/src/pages/ShortcutRedirector.tsx
Normal file
43
web/src/pages/ShortcutRedirector.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import Header from "../components/Header";
|
||||||
|
import { getShortcutWithNameAndWorkspaceName } from "../helpers/api";
|
||||||
|
import useLoading from "../hooks/useLoading";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
errMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShortcutRedirector: React.FC = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const [state, setState] = useState<State>();
|
||||||
|
const loadingState = useLoading();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const workspaceName = params.workspaceName || "";
|
||||||
|
const shortcutName = params.shortcutName || "";
|
||||||
|
getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName)
|
||||||
|
.then(({ data: { data: shortcut } }) => {
|
||||||
|
if (shortcut) {
|
||||||
|
window.location.href = shortcut.link;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setState({
|
||||||
|
errMessage: err?.message || "Not found",
|
||||||
|
});
|
||||||
|
loadingState.setFinish();
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return loadingState.isLoading ? null : (
|
||||||
|
<div className="w-full h-full flex flex-col justify-start items-start">
|
||||||
|
<Header />
|
||||||
|
<div className="w-full pt-24 text-center font-mono text-xl">
|
||||||
|
<p>{state?.errMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShortcutRedirector;
|
@ -809,6 +809,13 @@ copy-anything@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what "^3.14.1"
|
is-what "^3.14.1"
|
||||||
|
|
||||||
|
copy-to-clipboard@^3.3.2:
|
||||||
|
version "3.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz#5b263ec2366224b100181dded7ce0579b340c107"
|
||||||
|
integrity sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==
|
||||||
|
dependencies:
|
||||||
|
toggle-selection "^1.0.6"
|
||||||
|
|
||||||
cross-spawn@^7.0.2:
|
cross-spawn@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
@ -2474,6 +2481,11 @@ to-regex-range@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
toggle-selection@^1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||||
|
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||||
|
|
||||||
tslib@^1.8.1:
|
tslib@^1.8.1:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user