mirror of
https://github.com/aykhans/slash-e.git
synced 2025-04-16 12:23:12 +00:00
feat: update shortcut redirector
This commit is contained in:
parent
6b3ff5e462
commit
700598d1a5
@ -1,18 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
type ShortcutOrganizer struct {
|
|
||||||
ShortcutID int
|
|
||||||
UserID int
|
|
||||||
Pinned bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShortcutOrganizerFind struct {
|
|
||||||
ShortcutID int
|
|
||||||
UserID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShortcutOrganizerUpsert struct {
|
|
||||||
ShortcutID int
|
|
||||||
UserID int
|
|
||||||
Pinned bool `json:"pinned"`
|
|
||||||
}
|
|
@ -80,7 +80,7 @@ func aclMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if common.HasPrefixes(path, "/api/ping", "/api/status") && c.Request().Method == http.MethodGet {
|
if common.HasPrefixes(path, "/api/ping", "/api/status", "/api/workspace") && c.Request().Method == http.MethodGet {
|
||||||
return next(c)
|
return next(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,17 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post shortcut request").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
existingShortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{
|
||||||
|
Name: &shortcutCreate.Name,
|
||||||
|
WorkspaceID: &shortcutCreate.WorkspaceID,
|
||||||
|
})
|
||||||
|
if err != nil && common.ErrorCode(err) != common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
|
||||||
|
}
|
||||||
|
if existingShortcut != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Shortcut with name %s already exists", shortcutCreate.Name))
|
||||||
|
}
|
||||||
|
|
||||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
|
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create shortcut").SetInternal(err)
|
||||||
@ -48,6 +59,29 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
|
||||||
}
|
}
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
shortcut, err := s.Store.FindShortcut(ctx, &api.ShortcutFind{
|
||||||
|
ID: &shortcutID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find shortcut").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
workspaceUser, err := s.Store.FindWordspaceUser(ctx, &api.WorkspaceUserFind{
|
||||||
|
UserID: &userID,
|
||||||
|
WorkspaceID: &shortcut.WorkspaceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find workspace user").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortcut.CreatorID != userID && workspaceUser.Role != api.RoleAdmin {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "Forbidden to patch shortcut")
|
||||||
|
}
|
||||||
|
|
||||||
shortcutPatch := &api.ShortcutPatch{
|
shortcutPatch := &api.ShortcutPatch{
|
||||||
ID: shortcutID,
|
ID: shortcutID,
|
||||||
@ -56,7 +90,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch shortcut request").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcut, err := s.Store.PatchShortcut(ctx, shortcutPatch)
|
shortcut, err = s.Store.PatchShortcut(ctx, shortcutPatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch shortcut").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ CREATE TABLE shortcut (
|
|||||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
workspace_id INTEGER NOT NULL,
|
workspace_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
link TEXT NOT NULL DEFAULT '',
|
link TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||||
);
|
);
|
||||||
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
sqlite_sequence (name, seq)
|
sqlite_sequence (name, seq)
|
||||||
VALUES
|
VALUES
|
||||||
('shortcut', 1000);
|
('shortcut', 1000);
|
||||||
|
|
||||||
-- shortcut_organizer
|
|
||||||
CREATE TABLE shortcut_organizer (
|
|
||||||
shortcut_id INTEGER NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
|
||||||
UNIQUE(shortcut_id, user_id)
|
|
||||||
);
|
|
@ -70,7 +70,7 @@ CREATE TABLE shortcut (
|
|||||||
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL',
|
||||||
workspace_id INTEGER NOT NULL,
|
workspace_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
link TEXT NOT NULL DEFAULT '',
|
link TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
visibility TEXT NOT NULL CHECK (visibility IN ('PRIVATE', 'WORKSPACE', 'PUBLIC')) DEFAULT 'PRIVATE'
|
||||||
);
|
);
|
||||||
@ -78,12 +78,4 @@ CREATE TABLE shortcut (
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
sqlite_sequence (name, seq)
|
sqlite_sequence (name, seq)
|
||||||
VALUES
|
VALUES
|
||||||
('shortcut', 1000);
|
('shortcut', 1000);
|
||||||
|
|
||||||
-- shortcut_organizer
|
|
||||||
CREATE TABLE shortcut_organizer (
|
|
||||||
shortcut_id INTEGER NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0,
|
|
||||||
UNIQUE(shortcut_id, user_id)
|
|
||||||
);
|
|
@ -1,6 +1,3 @@
|
|||||||
DELETE FROM
|
|
||||||
shortcut_organizer;
|
|
||||||
|
|
||||||
DELETE FROM
|
DELETE FROM
|
||||||
shortcut;
|
shortcut;
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { Tooltip } from "@mui/joy";
|
import { Tooltip } from "@mui/joy";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { UNKNOWN_ID } from "../helpers/consts";
|
||||||
import { shortcutService, workspaceService } from "../services";
|
import { shortcutService, workspaceService } from "../services";
|
||||||
import { useAppSelector } from "../store";
|
import { useAppSelector } from "../store";
|
||||||
import { UNKNOWN_ID } from "../helpers/consts";
|
import { unknownWorkspace, unknownWorkspaceUser } from "../store/modules/workspace";
|
||||||
|
import { absolutifyLink } from "../helpers/utils";
|
||||||
import { showCommonDialog } from "./Alert";
|
import { showCommonDialog } from "./Alert";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
import toastHelper from "./Toast";
|
||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import CreateShortcutDialog from "./CreateShortcutDialog";
|
import CreateShortcutDialog from "./CreateShortcutDialog";
|
||||||
|
|
||||||
@ -20,14 +23,22 @@ interface State {
|
|||||||
|
|
||||||
const ShortcutListView: React.FC<Props> = (props: Props) => {
|
const ShortcutListView: React.FC<Props> = (props: Props) => {
|
||||||
const { workspaceId, shortcutList } = props;
|
const { workspaceId, shortcutList } = props;
|
||||||
const { user } = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user.user as User);
|
||||||
|
const { workspaceList } = useAppSelector((state) => state.workspace);
|
||||||
const [state, setState] = useState<State>({
|
const [state, setState] = useState<State>({
|
||||||
currentEditingShortcutId: UNKNOWN_ID,
|
currentEditingShortcutId: UNKNOWN_ID,
|
||||||
});
|
});
|
||||||
|
const workspace = workspaceList.find((workspace) => workspace.id === workspaceId) ?? unknownWorkspace;
|
||||||
|
const workspaceUser = workspace.workspaceUserList.find((workspaceUser) => workspaceUser.userId === user.id) ?? unknownWorkspaceUser;
|
||||||
|
|
||||||
|
const havePermission = (shortcut: Shortcut) => {
|
||||||
|
return workspaceUser.role === "ADMIN" || shortcut.creatorId === user.id;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopyButtonClick = (shortcut: Shortcut) => {
|
const handleCopyButtonClick = (shortcut: Shortcut) => {
|
||||||
const workspace = workspaceService.getWorkspaceById(workspaceId);
|
const workspace = workspaceService.getWorkspaceById(workspaceId);
|
||||||
copy(`${location.host}/${workspace?.name}/go/${shortcut.name}`);
|
copy(absolutifyLink(`/${workspace?.name}/${shortcut.name}`));
|
||||||
|
toastHelper.error("Shortcut link copied to clipboard.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditShortcutButtonClick = (shortcut: Shortcut) => {
|
const handleEditShortcutButtonClick = (shortcut: Shortcut) => {
|
||||||
@ -79,14 +90,14 @@ const ShortcutListView: React.FC<Props> = (props: Props) => {
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
disabled={shortcut.creatorId !== user?.id}
|
disabled={!havePermission(shortcut)}
|
||||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
className="w-full px-3 text-left leading-10 cursor-pointer rounded hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||||
onClick={() => handleEditShortcutButtonClick(shortcut)}
|
onClick={() => handleEditShortcutButtonClick(shortcut)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={shortcut.creatorId !== user?.id}
|
disabled={!havePermission(shortcut)}
|
||||||
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
className="w-full px-3 text-left leading-10 cursor-pointer rounded text-red-600 hover:bg-gray-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:opacity-60"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleDeleteShortcutButtonClick(shortcut);
|
handleDeleteShortcutButtonClick(shortcut);
|
||||||
|
@ -4,49 +4,8 @@ export const isNullorUndefined = (value: any) => {
|
|||||||
return isNull(value) || isUndefined(value);
|
return isNull(value) || isUndefined(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getNowTimeStamp(): number {
|
export function absolutifyLink(rel: string): string {
|
||||||
return Date.now();
|
const anchor = document.createElement("a");
|
||||||
}
|
anchor.setAttribute("href", rel);
|
||||||
|
return anchor.href;
|
||||||
export function getOSVersion(): "Windows" | "MacOS" | "Linux" | "Unknown" {
|
|
||||||
const appVersion = navigator.userAgent;
|
|
||||||
let detectedOS: "Windows" | "MacOS" | "Linux" | "Unknown" = "Unknown";
|
|
||||||
|
|
||||||
if (appVersion.indexOf("Win") != -1) {
|
|
||||||
detectedOS = "Windows";
|
|
||||||
} else if (appVersion.indexOf("Mac") != -1) {
|
|
||||||
detectedOS = "MacOS";
|
|
||||||
} else if (appVersion.indexOf("Linux") != -1) {
|
|
||||||
detectedOS = "Linux";
|
|
||||||
}
|
|
||||||
|
|
||||||
return detectedOS;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function debounce(fn: FunctionType, delay: number) {
|
|
||||||
let timer: number | null = null;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(fn, delay);
|
|
||||||
} else {
|
|
||||||
timer = setTimeout(fn, delay);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function throttle(fn: FunctionType, delay: number) {
|
|
||||||
let valid = true;
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!valid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
valid = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
fn();
|
|
||||||
valid = true;
|
|
||||||
}, delay);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -1,37 +1,35 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
import { getShortcutWithNameAndWorkspaceName } from "../helpers/api";
|
import { getShortcutWithNameAndWorkspaceName } from "../helpers/api";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import { userService } from "../services";
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
errMessage?: string;
|
errMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShortcutRedirector: React.FC = () => {
|
const ShortcutRedirector: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [state, setState] = useState<State>();
|
const [state, setState] = useState<State>();
|
||||||
const loadingState = useLoading();
|
const loadingState = useLoading();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userService.getState().user) {
|
|
||||||
navigate("/user/auth");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceName = params.workspaceName || "";
|
const workspaceName = params.workspaceName || "";
|
||||||
const shortcutName = params.shortcutName || "";
|
const shortcutName = params.shortcutName || "";
|
||||||
getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName)
|
getShortcutWithNameAndWorkspaceName(workspaceName, shortcutName)
|
||||||
.then(({ data: { data: shortcut } }) => {
|
.then(({ data: { data: shortcut } }) => {
|
||||||
if (shortcut) {
|
if (shortcut) {
|
||||||
window.location.href = shortcut.link;
|
window.location.href = shortcut.link;
|
||||||
|
} else {
|
||||||
|
setState({
|
||||||
|
errMessage: "Not found",
|
||||||
|
});
|
||||||
|
loadingState.setFinish();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
setState({
|
setState({
|
||||||
errMessage: err?.message || "Not found",
|
errMessage: error.response.data.error || "Error occurred",
|
||||||
});
|
});
|
||||||
loadingState.setFinish();
|
loadingState.setFinish();
|
||||||
});
|
});
|
||||||
|
@ -62,21 +62,8 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:workspaceName/go/:shortcutName",
|
path: "/:workspaceName/:shortcutName",
|
||||||
element: <ShortcutRedirector />,
|
element: <ShortcutRedirector />,
|
||||||
loader: async () => {
|
|
||||||
try {
|
|
||||||
await userService.initialState();
|
|
||||||
await workspaceService.fetchWorkspaceList();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user } = userService.getState();
|
|
||||||
if (isNullorUndefined(user)) {
|
|
||||||
return redirect("/user/auth");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user