Sfoglia il codice sorgente

feat: Add description for links fix #51 (#342)

* ✨ Add description for links fix #51

* feat: show description under original link

* feat: make advanced options input full-width

* fix: showing newly created link in the table

* fix: improve edit form style

* feat: add description migration

* fix: make description type optional

Co-authored-by: poeti8 <ezzati.upt@gmail.com>
glenn-louarn 5 anni fa
parent
commit
e492542428

+ 48 - 7
client/components/LinksTable.tsx

@@ -84,14 +84,14 @@ const Action = (props: React.ComponentProps<typeof Icon>) => (
 );
 
 const ogLinkFlex = { flexGrow: [1, 3, 7], flexShrink: [1, 3, 7] };
-const createdFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
+const createdFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] };
 const shortLinkFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
 const viewsFlex = {
   flexGrow: [0.5, 0.5, 1],
   flexShrink: [0.5, 0.5, 1],
   justifyContent: "flex-end"
 };
-const actionsFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] };
+const actionsFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
 
 interface RowProps {
   index: number;
@@ -109,6 +109,7 @@ interface BanForm {
 interface EditForm {
   target: string;
   address: string;
+  description: string;
 }
 
 const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
@@ -119,7 +120,8 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
   const [editFormState, { text, label }] = useFormState<EditForm>(
     {
       target: link.target,
-      address: link.address
+      address: link.address,
+      description: link.description
     },
     { withIds: true }
   );
@@ -175,7 +177,14 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
     <>
       <Tr key={link.id}>
         <Td {...ogLinkFlex} withFade>
-          <ALink href={link.target}>{link.target}</ALink>
+          <Col alignItems="flex-start">
+            <ALink href={link.target}>{link.target}</ALink>
+            {link.description && (
+              <Text fontSize={[13, 14]} color="#888">
+                {link.description}
+              </Text>
+            )}
+          </Col>
         </Td>
         <Td {...createdFlex}>{`${formatDistanceToNow(
           new Date(link.created_at)
@@ -292,9 +301,15 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
       </Tr>
       {showEdit && (
         <EditContent as="tr">
-          <Col as="td" alignItems="flex-start" px={[3, 3, 24]} py={[3, 3, 24]}>
-            <Flex alignItems="flex-start">
-              <Col alignItems="flex-start" mr={[0, 3, 3]}>
+          <Col
+            as="td"
+            alignItems="flex-start"
+            px={[3, 3, 24]}
+            py={[3, 3, 24]}
+            width={1}
+          >
+            <Flex alignItems="flex-start" width={1}>
+              <Col alignItems="flex-start" mr={3}>
                 <Text
                   {...label("target")}
                   as="label"
@@ -343,6 +358,32 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
                 </Flex>
               </Col>
             </Flex>
+            <Flex alignItems="flex-start" width={1} mt={3}>
+              <Col alignItems="flex-start">
+                <Text
+                  {...label("description")}
+                  as="label"
+                  mb={2}
+                  fontSize={[14, 15]}
+                  bold
+                >
+                  Description:
+                </Text>
+                <Flex as="form">
+                  <TextInput
+                    {...text("description")}
+                    placeholder="description..."
+                    placeholderSize={[13, 14]}
+                    fontSize={[14, 15]}
+                    height={[40, 44]}
+                    width={[1, 300, 420]}
+                    pl={[3, 24]}
+                    pr={[3, 24]}
+                    required
+                  />
+                </Flex>
+              </Col>
+            </Flex>
             <Button
               color="blue"
               mt={3}

+ 103 - 75
client/components/Shortener.tsx

@@ -51,6 +51,7 @@ interface Form {
   domain?: string;
   customurl?: string;
   password?: string;
+  description?: string;
   showAdvanced?: boolean;
 }
 
@@ -238,81 +239,108 @@ const Shortener = () => {
         alignSelf="flex-start"
       />
       {formState.values.showAdvanced && (
-        <Flex mt={4} flexDirection={["column", "row"]}>
-          <Col mb={[3, 0]}>
-            <Text
-              as="label"
-              {...label("domain")}
-              fontSize={[14, 15]}
-              mb={2}
-              bold
-            >
-              Domain
-            </Text>
-            <Select
-              {...select("domain")}
-              data-lpignore
-              pl={[3, 24]}
-              pr={[3, 24]}
-              fontSize={[14, 15]}
-              height={[40, 44]}
-              width={[170, 200]}
-              options={[
-                { key: defaultDomain, value: "" },
-                ...domains.map(d => ({
-                  key: d.address,
-                  value: d.address
-                }))
-              ]}
-            />
-          </Col>
-          <Col mb={[3, 0]} ml={[0, 24]}>
-            <Text
-              as="label"
-              {...label("customurl")}
-              fontSize={[14, 15]}
-              mb={2}
-              bold
-            >
-              {formState.values.domain || defaultDomain}/
-            </Text>
-            <TextInput
-              {...text("customurl")}
-              placeholder="Custom address..."
-              autocomplete="off"
-              data-lpignore
-              pl={[3, 24]}
-              pr={[3, 24]}
-              placeholderSize={[13, 14]}
-              fontSize={[14, 15]}
-              height={[40, 44]}
-              width={[210, 240]}
-            />
-          </Col>
-          <Col ml={[0, 24]}>
-            <Text
-              as="label"
-              {...label("password")}
-              fontSize={[14, 15]}
-              mb={2}
-              bold
-            >
-              Password:
-            </Text>
-            <TextInput
-              {...password("password")}
-              placeholder="Password..."
-              autocomplete="off"
-              data-lpignore
-              pl={[3, 24]}
-              pr={[3, 24]}
-              placeholderSize={[13, 14]}
-              fontSize={[14, 15]}
-              height={[40, 44]}
-              width={[210, 240]}
-            />
-          </Col>
-        </Flex>
+        <div>
+          <Flex mt={4} flexDirection={["column", "row"]}>
+            <Col mb={[3, 0]}>
+              <Text
+                as="label"
+                {...label("domain")}
+                fontSize={[14, 15]}
+                mb={2}
+                bold
+              >
+                Domain
+              </Text>
+              <Select
+                {...select("domain")}
+                data-lpignore
+                pl={[3, 24]}
+                pr={[3, 24]}
+                fontSize={[14, 15]}
+                height={[40, 44]}
+                width={[1, 210, 240]}
+                options={[
+                  { key: defaultDomain, value: "" },
+                  ...domains.map(d => ({
+                    key: d.address,
+                    value: d.address
+                  }))
+                ]}
+              />
+            </Col>
+            <Col mb={[3, 0]} ml={[0, 24]}>
+              <Text
+                as="label"
+                {...label("customurl")}
+                fontSize={[14, 15]}
+                mb={2}
+                bold
+              >
+                {formState.values.domain || defaultDomain}/
+              </Text>
+              <TextInput
+                {...text("customurl")}
+                placeholder="Custom address..."
+                autocomplete="off"
+                data-lpignore
+                pl={[3, 24]}
+                pr={[3, 24]}
+                placeholderSize={[13, 14]}
+                fontSize={[14, 15]}
+                height={[40, 44]}
+                width={[1, 210, 240]}
+              />
+            </Col>
+            <Col ml={[0, 24]}>
+              <Text
+                as="label"
+                {...label("password")}
+                fontSize={[14, 15]}
+                mb={2}
+                bold
+              >
+                Password:
+              </Text>
+              <TextInput
+                {...password("password")}
+                placeholder="Password..."
+                autocomplete="off"
+                data-lpignore
+                pl={[3, 24]}
+                pr={[3, 24]}
+                placeholderSize={[13, 14]}
+                fontSize={[14, 15]}
+                height={[40, 44]}
+                width={[1, 210, 240]}
+              />
+            </Col>
+          </Flex>
+          <Flex mt={[3]} flexDirection={["column", "row"]}>
+            <Col width={1}>
+              <Text
+                as="description"
+                {...label("description")}
+                fontSize={[14, 15]}
+                mb={2}
+                bold
+              >
+                Description
+              </Text>
+              <TextInput
+                {...text("description")}
+                placeholder="Description"
+                data-lpignore
+                pl={[3, 24]}
+                pr={[3, 24]}
+                placeholderSize={[13, 14]}
+                fontSize={[14, 15]}
+                height={[40, 44]}
+                width={1}
+                maxWidth="100%"
+              />
+            </Col>
+          </Flex>
+        </div>
       )}
     </Col>
   );

+ 5 - 1
client/store/links.ts

@@ -15,6 +15,7 @@ export interface Link {
   domain?: string;
   domain_id?: number;
   password?: string;
+  description?: string;
   target: string;
   updated_at: string;
   user_id?: number;
@@ -42,6 +43,7 @@ export interface EditLink {
   id: string;
   target: string;
   address: string;
+  description: string;
 }
 
 export interface LinksQuery {
@@ -118,7 +120,9 @@ export const links: Links = {
     actions.update(res.data);
   }),
   add: action((state, payload) => {
-    state.items.pop();
+    if (state.items.length >= 10) {
+      state.items.pop();
+    }
     state.items.unshift(payload);
   }),
   set: action((state, payload) => {

+ 1 - 0
global.d.ts

@@ -76,6 +76,7 @@ interface Link {
   created_at: string;
   domain_id?: number;
   password?: string;
+  description?: string;
   target: string;
   updated_at: string;
   user_id?: number;

+ 1 - 0
package.json

@@ -11,6 +11,7 @@
     "build": "next build client/ && rimraf production-server && tsc --project tsconfig.json && copyfiles -f \"server/mail/*.html\" production-server/mail",
     "start": "npm run migrate && NODE_ENV=production node production-server/server.js",
     "migrate": "knex migrate:latest --env production",
+    "migrate:make": "knex migrate:make --env production",
     "lint": "eslint server/ --ext .js,.ts --fix",
     "lint:nofix": "eslint server/ --ext .js,.ts",
     "docs:build": "cd docs/api && tsc generate.ts --resolveJsonModule && node generate && cd ../.."

+ 4 - 2
server/handlers/links.ts

@@ -42,7 +42,7 @@ export const get: Handler = async (req, res) => {
 };
 
 export const create: Handler = async (req: CreateLinkReq, res) => {
-  const { reuse, password, customurl, target, domain } = req.body;
+  const { reuse, password, customurl, description, target, domain } = req.body;
   const domain_id = domain ? domain.id : null;
 
   const targetDomain = URL.parse(target).hostname;
@@ -85,6 +85,7 @@ export const create: Handler = async (req: CreateLinkReq, res) => {
     password,
     address,
     domain_id,
+    description,
     target,
     user_id: req.user && req.user.id
   });
@@ -99,7 +100,7 @@ export const create: Handler = async (req: CreateLinkReq, res) => {
 };
 
 export const edit: Handler = async (req, res) => {
-  const { address, target } = req.body;
+  const { address, target, description } = req.body;
 
   if (!address && !target) {
     throw new CustomError("Should at least update one field.");
@@ -142,6 +143,7 @@ export const edit: Handler = async (req, res) => {
     },
     {
       ...(address && { address }),
+      ...(description && { description }),
       ...(target && { target })
     }
   );

+ 1 - 0
server/handlers/types.d.ts

@@ -5,6 +5,7 @@ export interface CreateLinkReq extends Request {
     reuse?: boolean;
     password?: string;
     customurl?: string;
+    description?: string;
     domain?: Domain;
     target: string;
   };

+ 12 - 0
server/handlers/validators.ts

@@ -81,6 +81,12 @@ export const createLink = [
     .withMessage("Only users can use this field.")
     .isBoolean()
     .withMessage("Reuse must be boolean."),
+  body("description")
+    .optional()
+    .isString()
+    .trim()
+    .isLength({ min: 0, max: 2040 })
+    .withMessage("Description length must be between 0 and 2040."),
   body("domain")
     .optional()
     .custom(checkUser)
@@ -132,6 +138,12 @@ export const editLink = [
     .withMessage("Custom URL is not valid")
     .custom(value => !preservedUrls.some(url => url.toLowerCase() === value))
     .withMessage("You can't use this custom URL."),
+  body("description")
+    .optional()
+    .isString()
+    .trim()
+    .isLength({ min: 0, max: 2040 })
+    .withMessage("Description length must be between 0 and 2040."),
   param("id", "ID is invalid.")
     .exists({ checkFalsy: true, checkNull: true })
     .isLength({ min: 36, max: 36 })

+ 10 - 0
server/migrations/20200718124944_description.ts

@@ -0,0 +1,10 @@
+import * as Knex from "knex";
+
+export async function up(knex: Knex): Promise<any> {
+  const hasDescription = await knex.schema.hasColumn("links", "description");
+  if (!hasDescription) {
+    await knex.schema.alterTable("links", table => {
+      table.string("description");
+    });
+  }
+}

+ 1 - 0
server/models/link.ts

@@ -9,6 +9,7 @@ export async function createLinkTable(knex: Knex) {
       knex.raw('create extension if not exists "uuid-ossp"');
       table.increments("id").primary();
       table.string("address").notNullable();
+      table.string("description");
       table
         .boolean("banned")
         .notNullable()

+ 10 - 6
server/queries/link.ts

@@ -12,6 +12,7 @@ const selectable = [
   "links.domain_id",
   "links.updated_at",
   "links.password",
+  "links.description",
   "links.target",
   "links.visit_count",
   "links.user_id",
@@ -52,9 +53,10 @@ export const total = async (match: Match<Link>, params: TotalParams = {}) => {
   });
 
   if (params.search) {
-    query.andWhereRaw("links.address || ' ' || target ILIKE '%' || ? || '%'", [
-      params.search
-    ]);
+    query.andWhereRaw(
+      "links.description || ' '  || links.address || ' ' || target ILIKE '%' || ? || '%'",
+      [params.search]
+    );
   }
 
   const [{ count }] = await query.count("id");
@@ -77,9 +79,10 @@ export const get = async (match: Partial<Link>, params: GetParams) => {
     .orderBy("created_at", "desc");
 
   if (params.search) {
-    query.andWhereRaw("links.address || ' ' || target ILIKE '%' || ? || '%'", [
-      params.search
-    ]);
+    query.andWhereRaw(
+      "links.description || ' '  || links.address || ' ' || target ILIKE '%' || ? || '%'",
+      [params.search]
+    );
   }
 
   query.leftJoin("domains", "links.domain_id", "domains.id");
@@ -131,6 +134,7 @@ export const create = async (params: Create) => {
       domain_id: params.domain_id || null,
       user_id: params.user_id || null,
       address: params.address,
+      description: params.description || null,
       target: params.target
     },
     "*"