Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--format documentation
--color
--require spec_helper
--backtrace
23 changes: 18 additions & 5 deletions adminapp/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ import RoleCreatePage from "./pages/RoleCreatePage";
import RoleDetailPage from "./pages/RoleDetailPage";
import RoleEditPage from "./pages/RoleEditPage";
import RoleListPage from "./pages/RoleListPage";
import ShortUrlDetailPage from "./pages/ShortUrlDetailPage";
import ShortUrlEditPage from "./pages/ShortUrlEditPage";
import ShortUrlListPage from "./pages/ShortUrlListPage";
import SignInPage from "./pages/SignInPage";
import StaticStringCreatePage from "./pages/StaticStringCreatePage";
import StaticStringsNamespacePage from "./pages/StaticStringsNamespacePage";
Expand Down Expand Up @@ -410,7 +413,6 @@ function PageSwitch() {
path="/mobility-trip/:id/edit"
element={renderWithHocs(redirectIfUnauthed, withLayout(), MobilityTripEditPage)}
/>

<Route
exact
path="/offerings"
Expand Down Expand Up @@ -568,7 +570,6 @@ function PageSwitch() {
ProgramEnrollmentExclusionDetailPage
)}
/>

<Route
exact
path="/program-pricing/new"
Expand Down Expand Up @@ -814,7 +815,6 @@ function PageSwitch() {
OrganizationMembershipEditPage
)}
/>

<Route
exact
path="/membership-verifications"
Expand Down Expand Up @@ -842,7 +842,6 @@ function PageSwitch() {
OrganizationMembershipVerificationEditPage
)}
/>

<Route
exact
path="/messages"
Expand Down Expand Up @@ -901,7 +900,6 @@ function PageSwitch() {
path="/marketing-list/:id/edit"
element={renderWithHocs(redirectIfUnauthed, withLayout(), MarketingListEditPage)}
/>

<Route
exact
path="/marketing-sms-broadcasts"
Expand Down Expand Up @@ -984,6 +982,21 @@ function PageSwitch() {
StaticStringsNamespacePage
)}
/>
<Route
exact
path="/short-urls"
element={renderWithHocs(redirectIfUnauthed, withLayout(), ShortUrlListPage)}
/>
<Route
exact
path="/short-url/:id"
element={renderWithHocs(redirectIfUnauthed, withLayout(), ShortUrlDetailPage)}
/>
<Route
exact
path="/short-url/:id/edit"
element={renderWithHocs(redirectIfUnauthed, withLayout(), ShortUrlEditPage)}
/>
<Route
exact
path="/financials"
Expand Down
8 changes: 8 additions & 0 deletions adminapp/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ export default {
updateRole: ({ id, ...data }, ...args) =>
postForm(`/adminapi/v1/roles/${id}`, data, ...args),

getShortUrls: (data, ...args) => get(`/adminapi/v1/short_urls`, data, ...args),
createShortUrl: (data, ...args) =>
post(`/adminapi/v1/short_urls/create`, data, ...args),
getShortUrl: ({ id, ...data }, ...args) =>
post(`/adminapi/v1/short_urls/${id}`, data, ...args),
updateShortUrl: ({ id, ...data }, ...args) =>
post(`/adminapi/v1/short_urls/${id}`, data, ...args),

getStaticStrings: (data, ...args) => get(`/adminapi/v1/static_strings`, data, ...args),
createStaticString: (data, ...args) =>
post(`/adminapi/v1/static_strings/create`, data, ...args),
Expand Down
13 changes: 10 additions & 3 deletions adminapp/src/components/Copyable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import isNumber from "lodash/isNumber";
import { useSnackbar } from "notistack";
import React from "react";

export default function Copyable({ text, delay, inline, children }) {
export default function Copyable({
text,
delay,
inline,
iconProps,
buttonOnly,
children,
}) {
delay = isNumber(delay) ? delay : 2000;
const { enqueueSnackbar } = useSnackbar();

Expand All @@ -19,9 +26,9 @@ export default function Copyable({ text, delay, inline, children }) {
const sx = inline && { px: "0!important", minWidth: "40px" };
return (
<React.Fragment>
{children || text}
{buttonOnly ? null : children || text}
<Button title="Copy" variant="link" sx={sx} onClick={onCopy}>
<ContentCopyIcon />
<ContentCopyIcon {...iconProps} />
</Button>
</React.Fragment>
);
Expand Down
3 changes: 2 additions & 1 deletion adminapp/src/components/ResourceList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import pluralize from "../modules/pluralize";
import { resourceCreateRoute } from "../modules/resourceRoutes";
import useAsyncFetch from "../shared/react/useAsyncFetch";
import useListQueryControls from "../shared/react/useListQueryControls";
import humps from "humps";
import startCase from "lodash/startCase";
import React from "react";

Expand All @@ -28,7 +29,7 @@ export default function ResourceList({
page: page + 1,
perPage,
search,
orderBy,
orderBy: humps.decamelize(orderBy || "") || null,
orderDirection: order,
});
}, [apiList, order, orderBy, page, perPage, search]);
Expand Down
6 changes: 6 additions & 0 deletions adminapp/src/hooks/useNavLinks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import EventAvailableIcon from "@mui/icons-material/EventAvailable";
import HomeIcon from "@mui/icons-material/Home";
import HowToRegIcon from "@mui/icons-material/HowToReg";
import KeyIcon from "@mui/icons-material/Key";
import LinkIcon from "@mui/icons-material/Link";
import MailIcon from "@mui/icons-material/Mail";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";
import OutboxIcon from "@mui/icons-material/Outbox";
Expand Down Expand Up @@ -227,6 +228,11 @@ export default function useNavLinks() {
href: "/marketing-sms-dispatches",
icon: <OutboxIcon />,
},
{
label: "URL Shortener",
href: "/short-urls",
icon: <LinkIcon />,
},
].filter(Boolean),
},
];
Expand Down
23 changes: 23 additions & 0 deletions adminapp/src/pages/MemberDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export default function MemberDetailPage() {
<EnrollmentExclusions model={model} />,
<Activities activities={model.activities} />,
<Orders orders={model.orders} />,
<MobilityTrips mobilityTrips={model.mobilityTrips} />,
<Charges charges={model.charges} />,
<PaymentInstruments instruments={model.paymentInstruments} />,
<MessagePreferences preferences={model.preferences} />,
Expand Down Expand Up @@ -415,6 +416,28 @@ function Orders({ orders }) {
);
}

function MobilityTrips({ mobilityTrips }) {
return (
<RelatedList
title="Trips"
rows={mobilityTrips}
headers={["Id", "Began At", "Ended At", "Service", "Rate"]}
keyRowAttr="id"
toCells={(row) => [
<AdminLink key="id" model={row} />,
formatDate(row.beganAt),
formatDate(row.endedAt),
<AdminLink key="vs" model={row.vendorService}>
{row.vendorService.internalName}
</AdminLink>,
<AdminLink key="r" model={row.vendorServiceRate}>
{row.vendorServiceRate.internalName}
</AdminLink>,
]}
/>
);
}

function Charges({ charges }) {
return (
<RelatedList
Expand Down
25 changes: 25 additions & 0 deletions adminapp/src/pages/ShortUrlDetailPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import api from "../api";
import Copyable from "../components/Copyable";
import ResourceDetail from "../components/ResourceDetail";
import { dayjs } from "../modules/dayConfig";
import React from "react";

export default function ShortUrlDetailPage() {
return (
<ResourceDetail
resource="short_url"
apiGet={api.getShortUrl}
canEdit
properties={(model) => [
{ label: "ID", value: model.id },
{ label: "Short ID", value: model.shortId },
{
label: "Short URL",
value: <Copyable inline text={model.shortUrl} />,
},
{ label: "Long URL", value: model.longUrl },
{ label: "Timestamp", value: dayjs(model.insertedAt) },
]}
/>
);
}
51 changes: 51 additions & 0 deletions adminapp/src/pages/ShortUrlEditPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import api from "../api";
import FormLayout from "../components/FormLayout";
import ResourceEdit from "../components/ResourceEdit";
import { Stack, TextField } from "@mui/material";
import React from "react";

export default function ShortUrlEditPage() {
return (
<ResourceEdit
apiGet={api.getShortUrl}
apiUpdate={api.updateShortUrl}
Form={ShortUrlForm}
/>
);
}

function ShortUrlForm({ resource, setFieldFromInput, register, isBusy, onSubmit }) {
return (
<FormLayout
title="Update Short URL"
subtitle="This is a self-hosted URL shortener that can
be used like other url shorteners. Note that it does not
currently provide any analytics.
You should use analytics at the target page instead."
onSubmit={onSubmit}
isBusy={isBusy}
>
<Stack spacing={2}>
<TextField
{...register("shortId")}
label="Short ID"
name="shortId"
helperText="Leave blank to auto-generate a short ID."
value={resource.shortId}
fullWidth
onChange={setFieldFromInput}
/>
<TextField
{...register("longUrl")}
label="URL"
name="longUrl"
helperText="The URL to shorten."
value={resource.longUrl}
fullWidth
autoFocus
onChange={setFieldFromInput}
/>
</Stack>
</FormLayout>
);
}
70 changes: 70 additions & 0 deletions adminapp/src/pages/ShortUrlListPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import api from "../api";
import AdminLink from "../components/AdminLink";
import Copyable from "../components/Copyable";
import FabAdd from "../components/FabAdd";
import ResourceList from "../components/ResourceList";
import formatDate from "../modules/formatDate";
import SafeExternalLink from "../shared/react/SafeExternalLink";
import React from "react";
import { useNavigate } from "react-router-dom";

export default function ShortUrlListPage() {
const navigate = useNavigate();

function handleCreate() {
api.createShortUrl().then((r) => navigate(`/short-url/${r.data.id}/edit`));
}

return (
<>
<FabAdd onClick={handleCreate} />
<ResourceList
resource="short_url"
apiList={api.getShortUrls}
canSearch
columns={[
{
id: "id",
label: "ID",
align: "right",
sortable: true,
render: (c) => <AdminLink model={c} />,
},
{
id: "shortId",
label: "Short ID",
align: "left",
sortable: true,
render: (c) => (
<>
<Copyable
buttonOnly
inline
iconProps={{ fontSize: "small", color: "primary" }}
text={c.shortUrl}
/>
<AdminLink model={c}>{c.shortId}</AdminLink>
</>
),
},
{
id: "longUrl",
label: "URL",
align: "left",
sortable: true,
render: (c) => (
<SafeExternalLink href={c.longUrl}>{c.longUrl}</SafeExternalLink>
),
},
{
id: "insertedAt",
label: "Timestamp",
align: "left",
sortable: true,
render: (c) => formatDate(c.insertedAt),
},
]}
/>
</>
);
}
12 changes: 6 additions & 6 deletions db/migrations/049_url_shortner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

Sequel.migration do
up do
require "suma/url_shortener"

Suma::UrlShortener.new_shortener(conn: self).create_table
create_table(:url_shortener) do
column :short_id, :text, unique: true, null: false
column :url, :text, null: false
column :inserted_at, :timestamptz, null: false, default: Sequel.function(:now)
end
end

down do
require "suma/url_shortener"

drop_table Suma::UrlShortener.new_shortener.table
drop_table :url_shortener
end
end
9 changes: 9 additions & 0 deletions db/migrations/106_shortened_url_ids.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

Sequel.migration do
change do
alter_table(:url_shortener) do
add_column :id, :Bigserial, primary_key: true
end
end
end
10 changes: 9 additions & 1 deletion lib/suma/admin_api/access.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class Suma::AdminAPI::Access
Suma::Vendor => [:vendor, COMMERCE, MANAGEMENT],
}.freeze

OTHER_RESOURCES = [
[:short_url, ALL, MARKETING_SMS],
].freeze

class << self
def key(resource, rw) = rw == :read ? read_key(resource) : write_key(resource)
def read_key(resource) = can?(resource, 1)
Expand All @@ -61,9 +65,13 @@ def write_key(resource) = can?(resource, 2)
end

def as_json
return MAPPING.values.each_with_object({}) do |v, acc|
r = MAPPING.values.each_with_object({}) do |v, acc|
acc[v[0]] = [v[1], v[2]]
end
OTHER_RESOURCES.each do |v|
r[v[0]] = [v[1], v[2]]
end
return r
end
end
end
Loading