From 5b6522218f0d11a6c2b78e88b85871806443ca55 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Sun, 9 Feb 2025 23:03:55 +0700 Subject: [PATCH] feat: added option to tag posts --- .../src/api/routes/posts.controller.ts | 23 ++ apps/frontend/src/app/global.scss | 78 ++++-- .../components/launches/add.edit.model.tsx | 21 +- .../components/launches/calendar.context.tsx | 4 +- .../src/components/launches/calendar.tsx | 46 +++- .../launches/helpers/top.title.component.tsx | 8 +- .../launches/providers/x/x.provider.tsx | 1 - .../components/launches/tags.component.tsx | 243 ++++++++++++++++++ .../database/prisma/posts/posts.repository.ts | 98 ++++++- .../database/prisma/posts/posts.service.ts | 17 +- .../src/database/prisma/schema.prisma | 29 +++ .../src/dtos/posts/create.post.dto.ts | 63 +++-- .../src/dtos/posts/create.tag.dto.ts | 9 + .../src/form/color.picker.tsx | 12 +- 14 files changed, 582 insertions(+), 70 deletions(-) create mode 100644 apps/frontend/src/components/launches/tags.component.tsx create mode 100644 libraries/nestjs-libraries/src/dtos/posts/create.tag.dto.ts diff --git a/apps/backend/src/api/routes/posts.controller.ts b/apps/backend/src/api/routes/posts.controller.ts index 81307e60..c9866bcd 100644 --- a/apps/backend/src/api/routes/posts.controller.ts +++ b/apps/backend/src/api/routes/posts.controller.ts @@ -28,6 +28,7 @@ import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.s import { Response } from 'express'; import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.request'; import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service'; +import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto'; @ApiTags('Posts') @Controller('/posts') @@ -71,6 +72,28 @@ export class PostsController { return this._postsService.createComment(org.id, user.id, id, body.comment); } + @Get('/tags') + async getTags(@GetOrgFromRequest() org: Organization) { + return { tags: await this._postsService.getTags(org.id) }; + } + + @Post('/tags') + async createTag( + @GetOrgFromRequest() org: Organization, + @Body() body: CreateTagDto + ) { + return this._postsService.createTag(org.id, body); + } + + @Put('/tags/:id') + async editTag( + @GetOrgFromRequest() org: Organization, + @Body() body: CreateTagDto, + @Param('id') id: string + ) { + return this._postsService.editTag(id, org.id, body); + } + @Get('/') async getPosts( @GetOrgFromRequest() org: Organization, diff --git a/apps/frontend/src/app/global.scss b/apps/frontend/src/app/global.scss index 2c1b87fb..a6f84b97 100644 --- a/apps/frontend/src/app/global.scss +++ b/apps/frontend/src/app/global.scss @@ -1,8 +1,8 @@ @tailwind base; @tailwind components; @tailwind utilities; -@import "./colors.scss"; -@import "./polonto.css"; +@import './colors.scss'; +@import './polonto.css'; body, html { @@ -379,14 +379,14 @@ html { -webkit-text-fill-color: white !important; } div div .set-font-family { - font-family: "Helvetica Neue", Helvetica !important; + font-family: 'Helvetica Neue', Helvetica !important; font-stretch: 100% !important; font-style: normal !important; font-weight: 400 !important; } .col-calendar:hover:before { - content: "Date passed"; + content: 'Date passed'; color: white; position: absolute; left: 50%; @@ -397,26 +397,68 @@ div div .set-font-family { } .loading-shimmer { - position: relative; - color: rgba(255, 255, 255, .5); + position: relative; + color: rgba(255, 255, 255, 0.5); } .loading-shimmer:before { - content: attr(data-text); - position: absolute; - overflow: hidden; - max-width: 100%; - white-space: nowrap; - color: white; - animation: loading 4s linear 0s infinite; - filter: blur(0.4px); + content: attr(data-text); + position: absolute; + overflow: hidden; + max-width: 100%; + white-space: nowrap; + color: white; + animation: loading 4s linear 0s infinite; + filter: blur(0.4px); } @keyframes loading { - 0% { - max-width: 0; - } + 0% { + max-width: 0; + } } .tbaom7c { display: none; -} \ No newline at end of file +} + +.tags-top > div { + flex: 1; + margin-right: 20px; + border: 0 !important; + background: transparent !important; + padding-left: 0 !important; +} + +.tags-top .react-tags__combobox { + margin-left: 5px; +} + +.tags-top .react-tags__combobox { + height: 35px; + display: flex; + background-color: #141c2c; + padding-left: 10px; + padding-right: 10px; + min-width: 150px; + text-align: left; + border-width: 1px; + border-radius: 4px; + border-color: var(--color-fifth); +} + +.tags-top .react-tags__list, +.tags-top .react-tags__list li, +.tags-top .react-tags__list li > div { + height: 35px; +} + +.tags-top .react-tags__listbox { + z-index: 1000 !important; +} + +.tags-top .react-tags__list-item > div { + display: flex; + align-items: center; + padding-left: 5px; + padding-right: 5px; +} diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 58c10eb2..a986bb43 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -52,13 +52,12 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import Image from 'next/image'; import { weightedLength } from '@gitroom/helpers/utils/count.length'; -import { uniqBy } from 'lodash'; -import { Select } from '@gitroom/react/form/select'; import { useClickOutside } from '@gitroom/frontend/components/layout/click.outside'; import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader'; import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; import { DropFiles } from '@gitroom/frontend/components/layout/drop.files'; import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer'; +import { TagsComponent } from './tags.component'; function countCharacters(text: string, type: string): number { if (type !== 'x') { @@ -141,6 +140,14 @@ export const AddEditModal: FC<{ // are we in edit mode? const existingData = useExistingData(); + const [tags, setTags] = useState( + // @ts-ignore + existingData?.posts?.[0]?.tags?.map((p: any) => ({ + label: p.tag.name, + value: p.tag.name, + })) || [] + ); + // Post for const [postFor, setPostFor] = useState(); @@ -387,6 +394,7 @@ export const AddEditModal: FC<{ body: JSON.stringify({ ...(postFor ? { order: postFor.id } : {}), type, + tags, shortLink, date: dateState.utc().format('YYYY-MM-DDTHH:mm:ss'), posts: allKeys.map((p) => ({ @@ -416,6 +424,7 @@ export const AddEditModal: FC<{ integrations, existingData, selectedIntegrations, + tags, ] ); @@ -814,7 +823,13 @@ export const AddEditModal: FC<{ )} >
- + + setTags(e.target.value)} + /> , integrations: [] as (Integrations & { refreshNeeded?: boolean })[], trendings: [] as string[], - posts: [] as Array, + posts: [] as Array, reloadCalendarView: () => { /** empty **/ }, diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index ab750e75..fd259fdb 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -21,7 +21,7 @@ import clsx from 'clsx'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { ExistingDataContextProvider } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; import { useDrag, useDrop } from 'react-dnd'; -import { Integration, Post, State } from '@prisma/client'; +import { Integration, Post, State, Tags, TagsPosts } from '@prisma/client'; import { useAddProvider } from '@gitroom/frontend/components/launches/add.provider.component'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { useUser } from '@gitroom/frontend/components/layout/user.context'; @@ -634,7 +634,9 @@ export const CalendarColumn: FC<{ )} > = memo((props) => { const { editPost, @@ -711,21 +713,45 @@ const CalendarItem: FC<{ className={clsx('w-full flex h-full flex-1 flex-col group', 'relative')} style={{ opacity }} > -
+
+ {post.tags.map((p) => p.tag.name).join(', ')} +
+
{' '}
@@ -778,7 +804,7 @@ const Duplicate = () => { > ); @@ -797,7 +823,7 @@ const Preview = () => { > ); @@ -816,7 +842,7 @@ export const Statistics = () => { > ); diff --git a/apps/frontend/src/components/launches/helpers/top.title.component.tsx b/apps/frontend/src/components/launches/helpers/top.title.component.tsx index 68942ef3..d851d181 100644 --- a/apps/frontend/src/components/launches/helpers/top.title.component.tsx +++ b/apps/frontend/src/components/launches/helpers/top.title.component.tsx @@ -3,15 +3,17 @@ import { FC, ReactNode } from 'react'; export const TopTitle: FC<{ title: string; shouldExpend?: boolean; + removeTitle?: boolean; expend?: () => void; collapse?: () => void; children?: ReactNode; }> = (props) => { - const { title, children, shouldExpend, expend, collapse } = props; + const { title, removeTitle, children, shouldExpend, expend, collapse } = + props; return (
-
{title}
+ {!removeTitle &&
{title}
} {children} {shouldExpend !== undefined && (
@@ -48,4 +50,4 @@ export const TopTitle: FC<{ )}
); -}; \ No newline at end of file +}; diff --git a/apps/frontend/src/components/launches/providers/x/x.provider.tsx b/apps/frontend/src/components/launches/providers/x/x.provider.tsx index 78f141b7..e00df91a 100644 --- a/apps/frontend/src/components/launches/providers/x/x.provider.tsx +++ b/apps/frontend/src/components/launches/providers/x/x.provider.tsx @@ -28,7 +28,6 @@ export default withProvider( }, (settings) => { if (settings?.[0]?.value) { - console.log(4000); return 4000; } return 280; diff --git a/apps/frontend/src/components/launches/tags.component.tsx b/apps/frontend/src/components/launches/tags.component.tsx new file mode 100644 index 00000000..ce224d45 --- /dev/null +++ b/apps/frontend/src/components/launches/tags.component.tsx @@ -0,0 +1,243 @@ +import { FC, useCallback, useMemo, useState } from 'react'; +import { ReactTags } from 'react-tag-autocomplete'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; +import { Input } from '@gitroom/react/form/input'; +import { ColorPicker } from '@gitroom/react/form/color.picker'; +import { Button } from '@gitroom/react/form/button'; +import { uniqBy } from 'lodash'; + +export const TagsComponent: FC<{ + name: string; + label: string; + initial: any[]; + onChange: (event: { target: { value: any[]; name: string } }) => void; +}> = (props) => { + const { onChange, name, initial } = props; + const fetch = useFetch(); + const [tagValue, setTagValue] = useState(initial?.slice(0) || []); + const [suggestions, setSuggestions] = useState(''); + const [showModal, setShowModal] = useState(false); + + const loadTags = useCallback(async () => { + return (await fetch('/posts/tags')).json(); + }, []); + + const { isLoading, data, mutate } = useSWR<{ + tags: { name: string; color: string }[]; + }>('tags', loadTags); + + const onDelete = useCallback( + (tagIndex: number) => { + const modify = tagValue.filter((_, i) => i !== tagIndex); + setTagValue(modify); + onChange({ target: { value: modify, name } }); + }, + [tagValue] + ); + + const createNewTag = useCallback( + async (newTag: any) => { + const val = await new Promise((resolve) => { + setShowModal({ + tag: newTag.value, + resolve, + close: () => setShowModal(false), + }); + }); + + setShowModal(false); + mutate(); + + return val; + }, + [mutate] + ); + + const edit = useCallback( + (tag: any) => async (e: any) => { + e.stopPropagation(); + e.preventDefault(); + const val = await new Promise((resolve) => { + setShowModal({ + tag: tag.name, + color: tag.color, + id: tag.id, + resolve, + close: () => setShowModal(false), + }); + }); + + setShowModal(false); + mutate(); + + const modify = tagValue.map((t) => { + if (t.label === tag.name) { + return { value: val, label: val }; + } + + return t; + }); + + setTagValue(modify); + onChange({ target: { value: modify, name } }); + }, + [tagValue, data] + ); + + const onAddition = useCallback( + async (newTag: any) => { + if (tagValue.length >= 3) { + return; + } + + const getTag = data?.tags?.find((f) => f.name === newTag.label) + ? newTag.label + : await createNewTag(newTag); + + const modify = [...tagValue, { value: getTag, label: getTag }]; + setTagValue(modify); + onChange({ target: { value: modify, name } }); + }, + [tagValue, data] + ); + + // useEffect(() => { + // const settings = getValues()[props.name]; + // if (settings) { + // setTagValue(settings); + // } + // }, []); + + const suggestionsArray = useMemo(() => { + return uniqBy<{ label: string; value: string }>( + [ + ...(data?.tags.map((p) => ({ + label: p.name, + value: p.name, + })) || []), + ...tagValue, + { label: suggestions, value: suggestions }, + ].filter((f) => f.label), + (o) => o.label + ); + }, [suggestions, tagValue]); + + if (isLoading) { + return null; + } + + return ( + <> + {showModal && } +
+ { + const findTag = data?.tags?.find((f) => f.name === tag.tag.label); + const findIndex = tagValue.findIndex( + (f) => f.label === tag.tag.label + ); + return ( +
+
+ Edit +
+
onDelete(findIndex)} + > + X +
+
+ {tag.tag.label} +
+
+ ); + }} + /> +
+ + ); +}; + +const ShowModal: FC<{ + tag: string; + color?: string; + id?: string; + close: () => void; + resolve: (value: string) => void; +}> = (props) => { + const { close, tag, resolve, color: theColor, id } = props; + const fetch = useFetch(); + const [color, setColor] = useState(theColor || '#942828'); + const [tagName, setTagName] = useState(tag); + const save = useCallback(async () => { + await fetch(id ? `/posts/tags/${id}` : '/posts/tags', { + method: id ? 'PUT' : 'POST', + body: JSON.stringify({ name: tagName, color }), + }); + + resolve(tagName); + }, [tagName, color, id]); + return ( +
+
+ + + +
+ setTagName(e.target.value)} + /> + setColor(e.target.value)} + label="Tag Color" + name="color" + value={color} + enabled={true} + canBeCancelled={false} + /> + +
+
+
+ ); +}; diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 825deb8e..28e6b9e1 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -7,6 +7,7 @@ import dayjs from 'dayjs'; import isoWeek from 'dayjs/plugin/isoWeek'; import weekOfYear from 'dayjs/plugin/weekOfYear'; import { v4 as uuidv4 } from 'uuid'; +import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto'; dayjs.extend(isoWeek); dayjs.extend(weekOfYear); @@ -16,7 +17,9 @@ export class PostsRepository { constructor( private _post: PrismaRepository<'post'>, private _popularPosts: PrismaRepository<'popularPosts'>, - private _comments: PrismaRepository<'comments'> + private _comments: PrismaRepository<'comments'>, + private _tags: PrismaRepository<'tags'>, + private _tagsPosts: PrismaRepository<'tagsPosts'> ) {} getOldPosts(orgId: string, date: string) { @@ -121,11 +124,13 @@ export class PostsRepository { }, deletedAt: null, parentPostId: null, - ...query.customer ? { - integration: { - customerId: query.customer, - } - }: {}, + ...(query.customer + ? { + integration: { + customerId: query.customer, + }, + } + : {}), }, select: { id: true, @@ -135,6 +140,11 @@ export class PostsRepository { submittedForOrganizationId: true, submittedForOrderId: true, state: true, + tags: { + select: { + tag: true, + }, + }, integration: { select: { id: true, @@ -186,6 +196,11 @@ export class PostsRepository { ...(includeIntegration ? { integration: true, + tags: { + select: { + tag: true, + }, + }, } : {}), childrenPost: true, @@ -256,7 +271,8 @@ export class PostsRepository { state: 'draft' | 'schedule' | 'now', orgId: string, date: string, - body: PostBody + body: PostBody, + tags: { value: string; label: string }[] ) { const posts: Post[] = []; const uuid = uuidv4(); @@ -315,6 +331,44 @@ export class PostsRepository { }, }) ); + + if (posts.length === 1) { + await this._tagsPosts.model.tagsPosts.deleteMany({ + where: { + post: { + id: posts[0].id, + }, + }, + }); + + if (tags.length) { + const tagsList = await this._tags.model.tags.findMany({ + where: { + orgId: orgId, + name: { + in: tags.map((tag) => tag.label), + }, + }, + }); + + if (tagsList.length) { + await this._post.model.post.update({ + where: { + id: posts[posts.length - 1].id, + }, + data: { + tags: { + createMany: { + data: tagsList.map((tag) => ({ + tagId: tag.id, + })), + }, + }, + }, + }); + } + } + } } const previousPost = body.group @@ -500,6 +554,36 @@ export class PostsRepository { }); } + async getTags(orgId: string) { + return this._tags.model.tags.findMany({ + where: { + orgId, + }, + }); + } + + createTag(orgId: string, body: CreateTagDto) { + return this._tags.model.tags.create({ + data: { + orgId, + name: body.name, + color: body.color, + }, + }); + } + + editTag(id: string, orgId: string, body: CreateTagDto) { + return this._tags.model.tags.update({ + where: { + id, + }, + data: { + name: body.name, + color: body.color, + }, + }); + } + createComment( orgId: string, userId: string, diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts index a3041482..0964994b 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts @@ -23,6 +23,7 @@ import utc from 'dayjs/plugin/utc'; import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service'; import { ShortLinkService } from '@gitroom/nestjs-libraries/short-linking/short.link.service'; import { WebhooksService } from '@gitroom/nestjs-libraries/database/prisma/webhooks/webhooks.service'; +import { CreateTagDto } from '@gitroom/nestjs-libraries/dtos/posts/create.tag.dto'; dayjs.extend(utc); type PostWithConditionals = Post & { @@ -595,7 +596,8 @@ export class PostsService { body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date, - post + post, + body.tags ); if (!posts?.length) { @@ -792,6 +794,7 @@ export class PostsService { date: randomDate, order: '', shortLink: false, + tags: [], posts: [ { group, @@ -884,6 +887,18 @@ export class PostsService { return this._postRepository.getComments(postId); } + getTags(orgId: string) { + return this._postRepository.getTags(orgId); + } + + createTag(orgId: string, body: CreateTagDto) { + return this._postRepository.createTag(orgId, body); + } + + editTag(id: string, orgId: string, body: CreateTagDto) { + return this._postRepository.editTag(id, orgId, body); + } + createComment( orgId: string, userId: string, diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index 31acc584..d9724fbe 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -34,6 +34,34 @@ model Organization { plugs Plugs[] customers Customer[] webhooks Webhooks[] + tags Tags[] +} + +model Tags { + id String @id @default(uuid()) + name String + color String + orgId String + organization Organization @relation(fields: [orgId], references: [id]) + posts TagsPosts[] + deletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([orgId]) + @@index([deletedAt]) +} + +model TagsPosts { + postId String + post Post @relation(fields: [postId], references: [id]) + tagId String + tag Tags @relation(fields: [tagId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@id([postId, tagId]) + @@unique([postId, tagId]) } model User { @@ -348,6 +376,7 @@ model Post { lastMessage Messages? @relation(fields: [lastMessageId], references: [id]) payoutProblems PayoutProblems[] comments Comments[] + tags TagsPosts[] error String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts index 3965f710..008958b5 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/create.post.dto.ts @@ -1,13 +1,23 @@ import { - ArrayMinSize, IsArray, IsBoolean, IsDateString, IsDefined, IsIn, IsOptional, IsString, MinLength, ValidateIf, ValidateNested + ArrayMinSize, + IsArray, + IsBoolean, + IsDateString, + IsDefined, + IsIn, + IsOptional, + IsString, + MinLength, + ValidateIf, + ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; import { DevToSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dev.to.settings.dto'; -import {MediaDto} from "@gitroom/nestjs-libraries/dtos/media/media.dto"; -import {AllProvidersSettings} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings"; -import {MediumSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto"; -import {HashnodeSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto"; -import {RedditSettingsDto} from "@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto"; +import { MediaDto } from '@gitroom/nestjs-libraries/dtos/media/media.dto'; +import { AllProvidersSettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings'; +import { MediumSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/medium.settings.dto'; +import { HashnodeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/hashnode.settings.dto'; +import { RedditSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/reddit.dto'; import { YoutubeSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/youtube.settings.dto'; import { PinterestSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/pinterest.dto'; import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/dribbble.dto'; @@ -36,8 +46,8 @@ export class PostContent { @IsArray() @IsOptional() @Type(() => MediaDto) - @ValidateNested({each: true}) - image: MediaDto[] + @ValidateNested({ each: true }) + image: MediaDto[]; } export class Post { @@ -63,23 +73,33 @@ export class Post { discriminator: { property: '__type', subTypes: [ - { value: DevToSettingsDto, name: 'devto' }, - { value: MediumSettingsDto, name: 'medium' }, - { value: HashnodeSettingsDto, name: 'hashnode' }, - { value: RedditSettingsDto, name: 'reddit' }, - { value: LemmySettingsDto, name: 'lemmy' }, - { value: YoutubeSettingsDto, name: 'youtube' }, - { value: PinterestSettingsDto, name: 'pinterest' }, - { value: DribbbleDto, name: 'dribbble' }, - { value: TikTokDto, name: 'tiktok' }, - { value: DiscordDto, name: 'discord' }, - { value: SlackDto, name: 'slack' }, + { value: DevToSettingsDto, name: 'devto' }, + { value: MediumSettingsDto, name: 'medium' }, + { value: HashnodeSettingsDto, name: 'hashnode' }, + { value: RedditSettingsDto, name: 'reddit' }, + { value: LemmySettingsDto, name: 'lemmy' }, + { value: YoutubeSettingsDto, name: 'youtube' }, + { value: PinterestSettingsDto, name: 'pinterest' }, + { value: DribbbleDto, name: 'dribbble' }, + { value: TikTokDto, name: 'tiktok' }, + { value: DiscordDto, name: 'discord' }, + { value: SlackDto, name: 'slack' }, ], }, }) settings: AllProvidersSettings; } +class Tags { + @IsDefined() + @IsString() + value: string; + + @IsDefined() + @IsString() + label: string; +} + export class CreatePostDto { @IsDefined() @IsIn(['draft', 'schedule', 'now']) @@ -97,6 +117,11 @@ export class CreatePostDto { @IsDateString() date: string; + @IsArray() + @IsDefined() + @ValidateNested({ each: true }) + tags: Tags[]; + @ValidateIf((o) => o.type !== 'draft') @IsDefined() @Type(() => Post) diff --git a/libraries/nestjs-libraries/src/dtos/posts/create.tag.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/create.tag.dto.ts new file mode 100644 index 00000000..a499e52a --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/posts/create.tag.dto.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator'; + +export class CreateTagDto { + @IsString() + name: string; + + @IsString() + color: string; +} diff --git a/libraries/react-shared-libraries/src/form/color.picker.tsx b/libraries/react-shared-libraries/src/form/color.picker.tsx index 6e929e6d..760cfa06 100644 --- a/libraries/react-shared-libraries/src/form/color.picker.tsx +++ b/libraries/react-shared-libraries/src/form/color.picker.tsx @@ -8,12 +8,14 @@ export const ColorPicker: FC<{ name: string; label: string; enabled: boolean; + onChange?: (params: { target: { name: string, value: string } }) => void; + value?: string; canBeCancelled: boolean; }> = (props) => { - const { name, label, enabled, canBeCancelled } = props; + const { name, label, enabled, value, canBeCancelled, onChange } = props; const form = useFormContext(); - const color = form.register(name); - const watch = form.watch(name); + const color = onChange ? {onChange} : form.register(name); + const watch = onChange ? value : form.watch(name); const [enabledState, setEnabledState] = useState(!!watch); const enable = useCallback(async () => { @@ -29,9 +31,7 @@ export const ColorPicker: FC<{ if (!enabledState) { return (
- +
); }