Skip to content

Commit

Permalink
Allow custom fonts in editor (#500)
Browse files Browse the repository at this point in the history
  • Loading branch information
pushchris authored Sep 15, 2024
1 parent a180ec4 commit da60db8
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 7 deletions.
20 changes: 20 additions & 0 deletions apps/platform/db/migrations/20240914230319_add_resources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
exports.up = async function(knex) {
await knex.schema.createTable('resources', function(table) {
table.increments()
table.integer('project_id')
.unsigned()
.notNullable()
.references('id')
.inTable('projects')
.onDelete('CASCADE')
table.string('type')
table.string('name')
table.json('value')
table.timestamp('created_at').defaultTo(knex.fn.now())
table.timestamp('updated_at').defaultTo(knex.fn.now())
})
}

exports.down = async function(knex) {
await knex.schema.dropTable('resources')
}
2 changes: 2 additions & 0 deletions apps/platform/src/config/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import AdminController from '../auth/AdminController'
import OrganizationController from '../organizations/OrganizationController'
import App from '../app'
import { organizationMiddleware } from '../organizations/OrganizationMiddleware'
import ResourceController from '../render/ResourceController'

export const register = (parent: Router, ...routers: Router[]) => {
for (const router of routers) {
Expand Down Expand Up @@ -100,6 +101,7 @@ export const projectRouter = (prefix?: string) => {
ProjectLocaleController,
UserController,
TagController,
ResourceController,
)
}

Expand Down
14 changes: 14 additions & 0 deletions apps/platform/src/render/Resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Model, { ModelParams } from '../core/Model'

export type ResourceType = 'font' | 'snippet'

export default class Resource extends Model {
project_id!: number
type!: ResourceType
name!: string
value!: Record<string, any>

static jsonAttributes = ['value']
}

export type ResourceParams = Omit<Resource, ModelParams>
55 changes: 55 additions & 0 deletions apps/platform/src/render/ResourceController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Router from '@koa/router'
import { ProjectState } from '../auth/AuthMiddleware'
import { JSONSchemaType, validate } from '../core/validate'
import Resource, { ResourceParams, ResourceType } from './Resource'
import { allResources, createResource, deleteResource, getResource } from './ResourceService'

const router = new Router<
ProjectState & { resource?: Resource }
>({
prefix: '/resources',
})

router.get('/', async ctx => {
const type = ctx.query.type as ResourceType
ctx.body = await allResources(ctx.state.project.id, type)
})

const resourceCreateParams: JSONSchemaType<ResourceParams> = {
$id: 'resourceCreateParams',
type: 'object',
required: ['type', 'name', 'value'],
properties: {
type: {
type: 'string',
enum: ['font', 'snippet'],
},
name: { type: 'string' },
value: {
type: 'object',
additionalProperties: true,
} as any,
},
additionalProperties: false,
}
router.post('/', async ctx => {
const payload = validate(resourceCreateParams, ctx.request.body)
ctx.body = await createResource(ctx.state.project.id, payload)
})

router.param('resourceId', async (value: string, ctx, next) => {
ctx.state.resource = await getResource(parseInt(value, 10), ctx.state.project.id)
if (!ctx.state.resource) {
ctx.throw(404)
return
}
return await next()
})

router.delete('/:resourceId', async ctx => {
const { id, project_id } = ctx.state.resource!
await deleteResource(id, project_id)
ctx.body = true
})

export default router
25 changes: 25 additions & 0 deletions apps/platform/src/render/ResourceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Resource, { ResourceParams, ResourceType } from './Resource'

export const allResources = async (projectId: number, type?: ResourceType): Promise<Resource[]> => {
return await Resource.all(qb => {
if (type) {
qb.where('type', type)
}
return qb.where('project_id', projectId)
})
}

export const getResource = async (id: number, projectId: number) => {
return await Resource.find(id, qb => qb.where('project_id', projectId))
}

export const createResource = async (projectId: number, params: ResourceParams) => {
return await Resource.insertAndFetch({
...params,
project_id: projectId,
})
}

export const deleteResource = async (id: number, projectId: number) => {
return await Resource.deleteById(id, qb => qb.where('project_id', projectId))
}
2 changes: 2 additions & 0 deletions apps/ui/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"action": "Action",
"add_admin": "Add Admin",
"add_admin_description": "Add a new admin to this organization. Admins have full access to all projects and settings, members can only access projects they are a part of.",
"add_font": "Add Font",
"add_list": "Add List",
"add_locale": "Add Locale",
"add_team_member": "Add Team Member",
Expand Down Expand Up @@ -128,6 +129,7 @@
"file": "File",
"finished": "Finished",
"first_name": "First Name",
"fonts": "Fonts",
"for_duration": "For a Duration",
"from_email": "From Email",
"from_name": "From Name",
Expand Down
2 changes: 2 additions & 0 deletions apps/ui/public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"action": "Acción",
"add_admin": "Añadir Admin",
"add_admin_description": "Añade un nuevo administrador a esta organización. Los administradores tienen acceso completo a todos los proyectos y ajustes, los miembros sólo pueden acceder a los proyectos de los que forman parte.",
"add_font": "Añadir Fuente",
"add_list": "Añadir lista",
"add_locale": "Añadir localidad",
"add_team_member": "Añadir miembro al equipo",
Expand Down Expand Up @@ -128,6 +129,7 @@
"file": "Archivo",
"finished": "Completo",
"first_name": "Nombre",
"fonts": "Fuentes",
"for_duration": "Por Una Duración",
"from_email": "Del Correo Electrónico",
"from_name": "Del Nombre",
Expand Down
14 changes: 13 additions & 1 deletion apps/ui/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Axios from 'axios'
import { env } from './config/env'
import { Admin, AuthMethod, Campaign, CampaignCreateParams, CampaignLaunchParams, CampaignUpdateParams, CampaignUser, Image, Journey, JourneyEntranceDetail, JourneyStepMap, JourneyUserStep, List, ListCreateParams, ListUpdateParams, Locale, Metric, Organization, OrganizationUpdateParams, Project, ProjectAdmin, ProjectAdminInviteParams, ProjectAdminParams, ProjectApiKey, ProjectApiKeyParams, Provider, ProviderCreateParams, ProviderMeta, ProviderUpdateParams, QueueMetric, RuleSuggestions, SearchParams, SearchResult, Subscription, SubscriptionCreateParams, SubscriptionParams, SubscriptionUpdateParams, Tag, Template, TemplateCreateParams, TemplatePreviewParams, TemplateProofParams, TemplateUpdateParams, User, UserEvent, UserSubscription } from './types'
import { Admin, AuthMethod, Campaign, CampaignCreateParams, CampaignLaunchParams, CampaignUpdateParams, CampaignUser, Image, Journey, JourneyEntranceDetail, JourneyStepMap, JourneyUserStep, List, ListCreateParams, ListUpdateParams, Locale, Metric, Organization, OrganizationUpdateParams, Project, ProjectAdmin, ProjectAdminInviteParams, ProjectAdminParams, ProjectApiKey, ProjectApiKeyParams, Provider, ProviderCreateParams, ProviderMeta, ProviderUpdateParams, QueueMetric, Resource, RuleSuggestions, SearchParams, SearchResult, Subscription, SubscriptionCreateParams, SubscriptionParams, SubscriptionUpdateParams, Tag, Template, TemplateCreateParams, TemplatePreviewParams, TemplateProofParams, TemplateUpdateParams, User, UserEvent, UserSubscription } from './types'

function appendValue(params: URLSearchParams, name: string, value: unknown) {
if (typeof value === 'undefined' || value === null || typeof value === 'function') return
Expand Down Expand Up @@ -286,6 +286,18 @@ const api = {
},
},

resources: {
all: async (projectId: number | string, type: string = 'font') => await client
.get<Resource[]>(`${projectUrl(projectId)}/resources?type=${type}`)
.then(r => r.data),
create: async (projectId: number | string, params: Partial<Resource>) => await client
.post<Resource>(`${projectUrl(projectId)}/resources`, params)
.then(r => r.data),
delete: async (projectId: number | string, id: number) => await client
.delete<number>(`${projectUrl(projectId)}/resources/${id}`)
.then(r => r.data),
},

tags: {
...createProjectEntityPath<Tag>('tags'),
used: async (projectId: number | string, entity: string) => await client
Expand Down
13 changes: 13 additions & 0 deletions apps/ui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,19 @@ export interface Image {
filesize: string
}

export interface Resource {
id: number
type: string
name: string
value: Record<string, any>
}

export interface Font {
name: string
url: string
value: string
}

export interface Tag {
id: number
name: string
Expand Down
57 changes: 57 additions & 0 deletions apps/ui/src/views/campaign/ResourceFontModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Modal, { ModalStateProps } from '../../ui/Modal'
import './ImageGalleryModal.css'
import { Font, Resource } from '../../types'
import { useTranslation } from 'react-i18next'
import FormWrapper from '../../ui/form/FormWrapper'
import TextInput from '../../ui/form/TextInput'
import api from '../../api'
import { useContext } from 'react'
import { ProjectContext } from '../../contexts'

interface ResourceModalProps extends ModalStateProps {
onInsert?: (resource: Resource) => void
}

export default function ResourceFontModal({ open, onClose, onInsert }: ResourceModalProps) {
const { t } = useTranslation()
const [project] = useContext(ProjectContext)

const handleCreateFont = async (params: Font) => {
const resource = await api.resources.create(project.id, {
type: 'font',
name: params.name,
value: params,
})
onInsert?.(resource)
}

return (
<Modal
title={t('add_font')}
open={open}
onClose={onClose}
size="small">
<FormWrapper<Font>
onSubmit={async (params) => { await handleCreateFont(params) }}
submitLabel={t('create')}>
{form => <>
<TextInput.Field
form={form}
name="name"
label={t('name')}
required />
<TextInput.Field
form={form}
name="value"
label="Font Family"
required />
<TextInput.Field
form={form}
name="url"
label="URL"
required />
</>}
</FormWrapper>
</Modal>
)
}
81 changes: 81 additions & 0 deletions apps/ui/src/views/campaign/ResourceModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Modal, { ModalStateProps } from '../../ui/Modal'
import { useContext, useState } from 'react'
import { ProjectContext } from '../../contexts'
import api from '../../api'
import './ImageGalleryModal.css'
import { Resource } from '../../types'
import { Button, DataTable, Heading } from '../../ui'
import { useTranslation } from 'react-i18next'
import ResourceFontModal from './ResourceFontModal'

interface ResourceModalProps extends ModalStateProps {
resources: Resource[]
setResources: (resources: Resource[]) => void
}

export default function ResourceModal({ open, onClose, resources, setResources }: ResourceModalProps) {
const { t } = useTranslation()
const [project] = useContext(ProjectContext)
const [showFontCreate, setShowFontCreate] = useState(false)

const handleRemove = async (id: number) => {
await api.resources.delete(project.id, id)
setResources(resources.filter(resource => resource.id !== id))
}

const handleAddResource = (resource: Resource) => {
setShowFontCreate(false)
setResources([...resources, resource])
}

return (
<Modal
title="Config"
open={open}
onClose={onClose}
size="large">
<Heading size="h4" title={t('fonts')} actions={
<Button size="small" onClick={() => setShowFontCreate(true)}>{t('add_font')}</Button>
} />
<div className="resources">
<DataTable
items={resources}
itemKey={({ item }) => item.id}
columns={[
{
key: 'name',
title: t('name'),
},
{
key: 'family',
title: 'Font Family',
cell: ({ item }) => item.value.value,
},
{
key: 'url',
title: 'URL',
cell: ({ item }) => item.value.url,
},
{
key: 'options',
title: t('options'),
cell: ({ item }) => (
<Button
size="small"
variant="destructive"
onClick={async () => await handleRemove(item.id)}>
{t('delete')}
</Button>
),
},
]} />
</div>

<ResourceFontModal
open={showFontCreate}
onClose={() => setShowFontCreate(false)}
onInsert={(resource) => handleAddResource(resource) }
/>
</Modal>
)
}
Loading

0 comments on commit da60db8

Please sign in to comment.