Merge 00ff779c6c into 125de91c71
commit
8773b03d29
|
|
@ -309,6 +309,10 @@
|
|||
"storage_template_enable_description": "Enable storage template engine",
|
||||
"storage_template_hash_verification_enabled": "Hash verification enabled",
|
||||
"storage_template_hash_verification_enabled_description": "Enables hash verification, don't disable this unless you're certain of the implications",
|
||||
"folder_content_order": "Folder content order",
|
||||
"folder_content_order_description": "Set the order in which assets are displayed in the folder view",
|
||||
"folder_content_order_name": "File name",
|
||||
"folder_content_order_date": "Creation date",
|
||||
"storage_template_migration": "Storage template migration",
|
||||
"storage_template_migration_description": "Apply the current <link>{template}</link> to previously uploaded assets",
|
||||
"storage_template_migration_info": "The storage template will convert all extensions to lowercase. Template changes will only apply to new assets. To retroactively apply the template to previously uploaded assets, run the <link>{job}</link>.",
|
||||
|
|
|
|||
|
|
@ -16554,6 +16554,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FolderContentOrder": {
|
||||
"enum": [
|
||||
"name",
|
||||
"date"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FoldersResponse": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -21972,6 +21979,13 @@
|
|||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"folderContentOrder": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/FolderContentOrder"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hashVerificationEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -21981,6 +21995,7 @@
|
|||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"folderContentOrder",
|
||||
"hashVerificationEnabled",
|
||||
"template"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1581,6 +1581,7 @@ export type SystemConfigServerDto = {
|
|||
};
|
||||
export type SystemConfigStorageTemplateDto = {
|
||||
enabled: boolean;
|
||||
folderContentOrder: FolderContentOrder;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
|
|
@ -5639,6 +5640,10 @@ export enum OAuthTokenEndpointAuthMethod {
|
|||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
}
|
||||
export enum FolderContentOrder {
|
||||
Name = "name",
|
||||
Date = "date"
|
||||
}
|
||||
export enum TriggerType {
|
||||
AssetCreate = "AssetCreate",
|
||||
PersonRecognized = "PersonRecognized"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
AudioCodec,
|
||||
Colorspace,
|
||||
CQMode,
|
||||
FolderContentOrder,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
OAuthTokenEndpointAuthMethod,
|
||||
|
|
@ -121,6 +122,7 @@ export interface SystemConfig {
|
|||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
folderContentOrder: FolderContentOrder;
|
||||
template: string;
|
||||
};
|
||||
image: {
|
||||
|
|
@ -311,6 +313,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
folderContentOrder: FolderContentOrder.Name,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
AudioCodec,
|
||||
CQMode,
|
||||
Colorspace,
|
||||
FolderContentOrder,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
OAuthTokenEndpointAuthMethod,
|
||||
|
|
@ -545,6 +546,9 @@ class SystemConfigStorageTemplateDto {
|
|||
@IsNotEmpty()
|
||||
@IsString()
|
||||
template!: string;
|
||||
|
||||
@ValidateEnum({ enum: FolderContentOrder, name: 'FolderContentOrder' })
|
||||
folderContentOrder!: FolderContentOrder;
|
||||
}
|
||||
|
||||
export class SystemConfigTemplateStorageOptionDto {
|
||||
|
|
|
|||
|
|
@ -873,3 +873,8 @@ export enum PluginTriggerType {
|
|||
AssetCreate = 'AssetCreate',
|
||||
PersonRecognized = 'PersonRecognized',
|
||||
}
|
||||
|
||||
export enum FolderContentOrder {
|
||||
Name = 'name',
|
||||
Date = 'date',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { AssetVisibility, FolderContentOrder } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { asUuid, withExif } from 'src/utils/database';
|
||||
|
||||
|
|
@ -26,8 +26,16 @@ export class ViewRepository {
|
|||
return results.map((row) => row.directoryPath.replaceAll(/\/$/g, ''));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string) {
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, FolderContentOrder.Name] })
|
||||
@GenerateSql({
|
||||
params: [DummyValue.UUID, DummyValue.STRING, FolderContentOrder.Date],
|
||||
name: 'getAssetsByOriginalPath (date order)',
|
||||
})
|
||||
async getAssetsByOriginalPath(
|
||||
userId: string,
|
||||
partialPath: string,
|
||||
order: FolderContentOrder = FolderContentOrder.Name,
|
||||
) {
|
||||
const normalizedPath = partialPath.replaceAll(/\/$/g, '');
|
||||
|
||||
return this.db
|
||||
|
|
@ -42,10 +50,13 @@ export class ViewRepository {
|
|||
.where('localDateTime', 'is not', null)
|
||||
.where('originalPath', 'like', `%${normalizedPath}/%`)
|
||||
.where('originalPath', 'not like', `%${normalizedPath}/%/%`)
|
||||
.orderBy(
|
||||
.$if(order === FolderContentOrder.Name, (qb) =>
|
||||
qb.orderBy(
|
||||
(eb) => eb.fn('regexp_replace', ['asset.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]),
|
||||
'asc',
|
||||
),
|
||||
)
|
||||
.$if(order === FolderContentOrder.Date, (qb) => qb.orderBy('fileCreatedAt', 'asc'))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
AudioCodec,
|
||||
Colorspace,
|
||||
CQMode,
|
||||
FolderContentOrder,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
OAuthTokenEndpointAuthMethod,
|
||||
|
|
@ -159,6 +160,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
folderContentOrder: FolderContentOrder.Name,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { FolderContentOrder } from 'src/enum';
|
||||
import { ViewService } from 'src/services/view.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
|
|
@ -44,6 +45,11 @@ describe(ViewService.name, () => {
|
|||
const result = await sut.getAssetsByOriginalPath(authStub.admin, path);
|
||||
expect(result).toEqual(mockAssetReponseDto);
|
||||
await expect(mocks.view.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets);
|
||||
expect(mocks.view.getAssetsByOriginalPath).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
path,
|
||||
FolderContentOrder.Name,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ export class ViewService extends BaseService {
|
|||
}
|
||||
|
||||
async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise<AssetResponseDto[]> {
|
||||
const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path);
|
||||
const { storageTemplate } = await this.getConfig({ withCache: true });
|
||||
const assets = await this.viewRepository.getAssetsByOriginalPath(
|
||||
auth.user.id,
|
||||
path,
|
||||
storageTemplate.folderContentOrder,
|
||||
);
|
||||
return assets.map((asset) => mapAsset(asset, { auth }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { AppRoute, SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { handleSystemConfigSave } from '$lib/services/system-config.service';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
||||
import { FolderContentOrder, getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
||||
import { LoadingSpinner } from '@immich/ui';
|
||||
import handlebar from 'handlebars';
|
||||
import * as luxon from 'luxon';
|
||||
|
|
@ -150,6 +151,19 @@
|
|||
configToEdit.storageTemplate.hashVerificationEnabled === config.storageTemplate.hashVerificationEnabled
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.folder_content_order')}
|
||||
desc={$t('admin.folder_content_order_description')}
|
||||
bind:value={configToEdit.storageTemplate.folderContentOrder}
|
||||
options={[
|
||||
{ value: FolderContentOrder.Name, text: $t('admin.folder_content_order_name') },
|
||||
{ value: FolderContentOrder.Date, text: $t('admin.folder_content_order_date') },
|
||||
]}
|
||||
isEdited={configToEdit.storageTemplate.folderContentOrder !== config.storageTemplate.folderContentOrder}
|
||||
{disabled}
|
||||
name="folder-content-order"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if configToEdit.storageTemplate.enabled}
|
||||
|
|
|
|||
Loading…
Reference in New Issue