mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-08-28 11:46:02 +08:00
Add soft deletes to providers (#642)
This commit is contained in:
parent
d1c0cf1500
commit
456a97851e
9 changed files with 84 additions and 19 deletions
|
@ -0,0 +1,11 @@
|
|||
exports.up = async function(knex) {
|
||||
await knex.schema.table('providers', function(table) {
|
||||
table.timestamp('deleted_at').nullable()
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('providers', function(table) {
|
||||
table.dropColumn('deleted_at')
|
||||
})
|
||||
}
|
|
@ -65,6 +65,7 @@ export default class Provider extends Model {
|
|||
rate_limit!: number
|
||||
rate_interval!: RateInterval
|
||||
setup!: ProviderSetupMeta[]
|
||||
deleted_at?: Date
|
||||
|
||||
static jsonAttributes = ['data']
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { analyticsProviders } from './analytics'
|
|||
import { emailProviders } from './email'
|
||||
import Provider from './Provider'
|
||||
import { getProvider } from './ProviderRepository'
|
||||
import { allProviders, loadController, pagedProviders } from './ProviderService'
|
||||
import { allProviders, archiveProvider, loadController, pagedProviders } from './ProviderService'
|
||||
import { pushProviders } from './push'
|
||||
import { textProviders } from './text'
|
||||
import { webhookProviders } from './webhook'
|
||||
|
@ -74,4 +74,8 @@ adminRouter.get('/meta', async ctx => {
|
|||
}))
|
||||
})
|
||||
|
||||
adminRouter.delete('/:id', async ctx => {
|
||||
ctx.body = await archiveProvider(parseInt(ctx.params.id), ctx.state.project.id)
|
||||
})
|
||||
|
||||
export { adminRouter, publicRouter, providers }
|
||||
|
|
|
@ -3,11 +3,11 @@ import { ProjectState } from '../auth/AuthMiddleware'
|
|||
import { PageParams } from '../core/searchParams'
|
||||
import { JSONSchemaType, validate } from '../core/validate'
|
||||
import Provider, { ProviderControllers, ProviderGroup, ProviderMeta, ProviderParams } from './Provider'
|
||||
import { createProvider, loadProvider, updateProvider } from './ProviderRepository'
|
||||
import { createProvider, getProvider, loadProvider, updateProvider } from './ProviderRepository'
|
||||
import App from '../app'
|
||||
|
||||
export const allProviders = async (projectId: number) => {
|
||||
return await Provider.all(qb => qb.where('project_id', projectId))
|
||||
return await Provider.all(qb => qb.where('project_id', projectId).whereNull('deleted_at'))
|
||||
}
|
||||
|
||||
export const hasProvider = async (projectId: number) => {
|
||||
|
@ -17,7 +17,7 @@ export const hasProvider = async (projectId: number) => {
|
|||
export const pagedProviders = async (params: PageParams, projectId: number) => {
|
||||
return await Provider.search(
|
||||
{ ...params, fields: ['name', 'group'] },
|
||||
b => b.where('project_id', projectId),
|
||||
b => b.where('project_id', projectId).whereNull('deleted_at'),
|
||||
App.main.db,
|
||||
(item) => {
|
||||
item.setup = item.loadSetup(App.main)
|
||||
|
@ -26,6 +26,11 @@ export const pagedProviders = async (params: PageParams, projectId: number) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const archiveProvider = async (id: number, projectId: number) => {
|
||||
await Provider.archive(id, qb => qb.where('project_id', projectId))
|
||||
return getProvider(id, projectId)
|
||||
}
|
||||
|
||||
export const loadController = (routers: ProviderControllers, provider: typeof Provider): ProviderMeta => {
|
||||
const { admin: adminRouter, public: publicRouter } = provider.controllers()
|
||||
if (routers.admin && adminRouter) {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"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_integration": "Add Integration",
|
||||
"add_list": "Add List",
|
||||
"add_locale": "Add Locale",
|
||||
"add_team_member": "Add Team Member",
|
||||
|
@ -52,6 +53,7 @@
|
|||
"create": "Create",
|
||||
"create_campaign": "Create Campaign",
|
||||
"create_journey": "Create Journey",
|
||||
"create_key": "Create Key",
|
||||
"create_list": "Create List",
|
||||
"create_locale": "Create Locale",
|
||||
"create_project": "Create Project",
|
||||
|
@ -77,6 +79,8 @@
|
|||
"delay_until": "Delay Until",
|
||||
"delete": "Delete",
|
||||
"delete_admin_confirmation": "Are you sure you want to delete this admin?",
|
||||
"delete_integration_confirmation": "Are you sure you want to archive this integration?",
|
||||
"delete_key_confirmation": "Are you sure you want to archive this key? All clients using the key will immediately be unable to access the API.",
|
||||
"delete_user": "Delete User",
|
||||
"delete_user_confirmation": "Are you sure you want to delete this user? All existing data will be removed.\n\nNote: If new data is sent for this user, they will be re-created with whatever data is sent.",
|
||||
"delivery": "Delivery",
|
||||
|
@ -259,6 +263,7 @@
|
|||
"save_settings": "Save Settings",
|
||||
"schedule": "Schedule",
|
||||
"scheduled": "Scheduled",
|
||||
"scope": "Scope",
|
||||
"search": "Search",
|
||||
"search_users": "Search by ID, email or phone",
|
||||
"second": "Second",
|
||||
|
@ -318,6 +323,7 @@
|
|||
"unsubscribe_all": "Unsubscribe From All",
|
||||
"until_date": "Until a Date",
|
||||
"until_time": "Until a Time",
|
||||
"update_key": "Update Key",
|
||||
"update_permissions": "Update Permissions",
|
||||
"update_subscription": "Update Subscription",
|
||||
"update_tag": "Update Tag",
|
||||
|
@ -337,6 +343,7 @@
|
|||
"users_change_subscription_status": "Are you sure you want to change the status of this subscription?",
|
||||
"users_count": "Users Count",
|
||||
"users_unsubscribe_all": "Are you sure you want to unsubscribe from all?",
|
||||
"value": "Value",
|
||||
"view_all": "View All",
|
||||
"visual": "Visual",
|
||||
"wait": "Wait",
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"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_integration": "Añadir Integración",
|
||||
"add_list": "Añadir lista",
|
||||
"add_locale": "Añadir localidad",
|
||||
"add_team_member": "Añadir miembro al equipo",
|
||||
|
@ -52,6 +53,7 @@
|
|||
"create": "Crear",
|
||||
"create_campaign": "Crear Campaña",
|
||||
"create_journey": "Crear Camino",
|
||||
"create_key": "Crear Clave",
|
||||
"create_list": "Crear Lista",
|
||||
"create_locale": "Crear localidad",
|
||||
"create_project": "Crear proyecto",
|
||||
|
@ -77,6 +79,8 @@
|
|||
"delay_until": "Espera Hasta",
|
||||
"delete": "Borrar",
|
||||
"delete_admin_confirmation": "¿Estás seguro de que deseas eliminar este administrador?",
|
||||
"delete_integration_confirmation": "¿Estás seguro de que deseas eliminar esta integración?",
|
||||
"delete_key_confirmation": "¿Estás seguro de que deseas eliminar esta clave?",
|
||||
"delete_user": "Borrar usuario",
|
||||
"delete_user_confirmation": "¿Estás seguro de que deseas eliminar este usuario? Todos los datos existentes serán eliminados. \n\n Nota: Si se envían datos nuevos para este usuario, se volverán a crear con los datos que se envíen.",
|
||||
"delivery": "Resultados",
|
||||
|
@ -259,6 +263,7 @@
|
|||
"save_settings": "Guardar ajustes",
|
||||
"schedule": "Planea Horario",
|
||||
"scheduled": "Programado",
|
||||
"scope": "Alcance",
|
||||
"search": "Buscar",
|
||||
"search_users": "Búsqueda por ID, correo electrónico o teléfono",
|
||||
"second": "Segundo",
|
||||
|
@ -277,6 +282,8 @@
|
|||
"sign_out": "Cerrar sesión",
|
||||
"sms_opt_out_message": "Mensaje SMS de exclusión",
|
||||
"sms_opt_out_message_subtitle": "Instrucciones sobre cómo cancelar la recepción de SMS que se adjuntarán a cada texto.",
|
||||
"sms_help_message": "Mensaje SMS de ajuda",
|
||||
"sms_help_message_subtitle": "Instrucciones sobre cómo recibir ayuda que se envían automáticamente a los usuarios si responden con AYUDA.",
|
||||
"start_journey": "Comienza el camino: ",
|
||||
"state": "Estado",
|
||||
"static": "Estático",
|
||||
|
@ -316,6 +323,7 @@
|
|||
"unsubscribe_all": "Darse de baja de todos",
|
||||
"until_date": "Hasta Una Fecha",
|
||||
"until_time": "Hasta un Tiempo",
|
||||
"update_key": "Actualizar Clave",
|
||||
"update_permissions": "Actualizar permisos",
|
||||
"update_subscription": "Actualizar suscripción",
|
||||
"update_tag": "Actualizar Etiqueta",
|
||||
|
@ -335,6 +343,7 @@
|
|||
"users_change_subscription_status": "¿Está seguro de que desea cambiar el estado de esta suscripción?",
|
||||
"users_count": "Número de usuarios",
|
||||
"users_unsubscribe_all": "¿Estás seguro de que quieres darte de baja de todos?",
|
||||
"value": "Valor",
|
||||
"view_all": "Ver Todo",
|
||||
"visual": "Visual",
|
||||
"wait": "Espere",
|
||||
|
|
|
@ -281,6 +281,9 @@ const api = {
|
|||
update: async (projectId: number | string, entityId: number | string, { group, type, ...provider }: ProviderUpdateParams) => await client
|
||||
.patch<Provider>(`${projectUrl(projectId)}/providers/${group}/${type}/${entityId}`, provider)
|
||||
.then(r => r.data),
|
||||
delete: async (projectId: number | string, id: number) => await client
|
||||
.delete<number>(`${projectUrl(projectId)}/providers/${id}`)
|
||||
.then(r => r.data),
|
||||
},
|
||||
|
||||
images: {
|
||||
|
|
|
@ -14,16 +14,17 @@ import { SingleSelect } from '../../ui/form/SingleSelect'
|
|||
import { snakeToTitle } from '../../utils'
|
||||
import { toast } from 'react-hot-toast/headless'
|
||||
import Alert from '../../ui/Alert'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ProjectApiKeys() {
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [project] = useContext(ProjectContext)
|
||||
const state = useSearchTableState(useCallback(async params => await api.apiKeys.search(project.id, params), [project]))
|
||||
|
||||
const [editing, setEditing] = useState<null | Partial<ProjectApiKey>>(null)
|
||||
|
||||
const handleArchive = async (id: number) => {
|
||||
if (confirm('Are you sure you want to archive this key? All clients using the key will immediately be unable to access the API.')) {
|
||||
if (confirm(t('delete_key_confirmation'))) {
|
||||
await api.apiKeys.delete(project.id, id)
|
||||
await state.reload()
|
||||
}
|
||||
|
@ -40,19 +41,22 @@ export default function ProjectApiKeys() {
|
|||
<SearchTable
|
||||
{...state}
|
||||
columns={[
|
||||
{ key: 'name' },
|
||||
{ key: 'name', title: t('name') },
|
||||
{
|
||||
key: 'scope',
|
||||
title: t('scope'),
|
||||
cell: ({ item }) => snakeToTitle(item.scope),
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
title: t('role'),
|
||||
cell: ({ item }) => item.scope === 'public'
|
||||
? undefined
|
||||
: snakeToTitle(item.role ?? ''),
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
title: t('value'),
|
||||
cell: ({ item }) => (
|
||||
<div className="cell-content">
|
||||
{item.value}
|
||||
|
@ -60,13 +64,17 @@ export default function ProjectApiKeys() {
|
|||
</div>
|
||||
),
|
||||
},
|
||||
{ key: 'description' },
|
||||
{
|
||||
key: 'description',
|
||||
title: t('description'),
|
||||
},
|
||||
{
|
||||
key: 'options',
|
||||
title: t('options'),
|
||||
cell: ({ item: { id } }) => (
|
||||
<Menu size="small">
|
||||
<MenuItem onClick={async () => await handleArchive(id)}>
|
||||
<ArchiveIcon />Archive
|
||||
<ArchiveIcon />{t('archive')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
),
|
||||
|
@ -74,19 +82,19 @@ export default function ProjectApiKeys() {
|
|||
]}
|
||||
itemKey={({ item }) => item.id}
|
||||
onSelectRow={setEditing}
|
||||
title="API Keys"
|
||||
title={t('api_keys')}
|
||||
actions={
|
||||
<Button
|
||||
icon={<PlusIcon />}
|
||||
size="small"
|
||||
onClick={() => setEditing({ scope: 'public', role: 'support' })}
|
||||
>
|
||||
Create Key
|
||||
{t('create_key')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={editing ? 'Update API Key' : 'Create API Key'}
|
||||
title={editing?.id ? t('update_key') : t('create_key')}
|
||||
open={Boolean(editing)}
|
||||
onClose={() => setEditing(null)}
|
||||
>
|
||||
|
@ -106,7 +114,7 @@ export default function ProjectApiKeys() {
|
|||
}
|
||||
}
|
||||
defaultValues={editing}
|
||||
submitLabel={editing?.id ? 'Update Key' : 'Create Key'}
|
||||
submitLabel={editing?.id ? t('update_key') : t('create_key')}
|
||||
>
|
||||
{
|
||||
form => {
|
||||
|
@ -116,18 +124,18 @@ export default function ProjectApiKeys() {
|
|||
<TextInput.Field
|
||||
form={form}
|
||||
name="name"
|
||||
label="Name"
|
||||
label={t('name')}
|
||||
required
|
||||
/>
|
||||
<TextInput.Field
|
||||
form={form}
|
||||
name="description"
|
||||
label="Description"
|
||||
label={t('description')}
|
||||
/>
|
||||
<RadioInput.Field
|
||||
form={form}
|
||||
name="scope"
|
||||
label="Scope"
|
||||
label={t('scope')}
|
||||
options={[
|
||||
{ key: 'public', label: 'Public' },
|
||||
{ key: 'secret', label: 'Secret' },
|
||||
|
@ -139,7 +147,7 @@ export default function ProjectApiKeys() {
|
|||
<SingleSelect.Field
|
||||
form={form}
|
||||
name="role"
|
||||
label="Role"
|
||||
label={t('role')}
|
||||
options={projectRoles}
|
||||
getOptionDisplay={snakeToTitle}
|
||||
required
|
||||
|
|
|
@ -4,10 +4,11 @@ import { ProjectContext } from '../../contexts'
|
|||
import { Provider } from '../../types'
|
||||
import Button from '../../ui/Button'
|
||||
import Heading from '../../ui/Heading'
|
||||
import { PlusIcon } from '../../ui/icons'
|
||||
import { ArchiveIcon, PlusIcon } from '../../ui/icons'
|
||||
import { SearchTable, useSearchTableState } from '../../ui/SearchTable'
|
||||
import IntegrationModal from './IntegrationModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Menu, MenuItem } from '../../ui'
|
||||
|
||||
export default function Integrations() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -15,6 +16,11 @@ export default function Integrations() {
|
|||
const state = useSearchTableState(useCallback(async params => await api.providers.search(project.id, params), [project]))
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [provider, setProvider] = useState<Provider>()
|
||||
const handleArchive = async (id: number) => {
|
||||
if (!confirm(t('delete_integration_confirmation'))) return
|
||||
await api.providers.delete(project.id, id)
|
||||
await state.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -22,7 +28,7 @@ export default function Integrations() {
|
|||
<Button icon={<PlusIcon />} size="small" onClick={() => {
|
||||
setProvider(undefined)
|
||||
setIsModalOpen(true)
|
||||
}}>Add Integration</Button>
|
||||
}}>{t('add_integration')}</Button>
|
||||
} />
|
||||
<SearchTable
|
||||
{...state}
|
||||
|
@ -31,6 +37,17 @@ export default function Integrations() {
|
|||
{ key: 'type', title: t('type') },
|
||||
{ key: 'group', title: t('group') },
|
||||
{ key: 'created_at', title: t('created_at') },
|
||||
{
|
||||
key: 'options',
|
||||
title: t('options'),
|
||||
cell: ({ item: { id } }) => (
|
||||
<Menu size="small">
|
||||
<MenuItem onClick={async () => await handleArchive(id)}>
|
||||
<ArchiveIcon />{t('archive')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
),
|
||||
},
|
||||
]}
|
||||
itemKey={({ item }) => item.id}
|
||||
onSelectRow={(provider: Provider) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue