mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload * updated fixtures with new property * if upload is 'read-only', ensure there is no existing asset at the designated originalPath * added test for file import as well as detecting existing image at read-only destination location * Added storage service test for a case where it should not move read-only assets * upload doesn't need the read-only flag available, just importing * default isReadOnly on import endpoint to true * formatting fixes * create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation * updated code to reflect changes in MR * fixed read stream promise return type * new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates * refactor: import asset * chore: open api * chore: tests * Added externalPath support for individual users, updated UI to allow this to be set by admin * added missing var for externalPath in ui * chore: open api * fix: compilation issues * fix: server test * built api, fixed user-response dto to include externalPath * reverted accidental commit * bad commit of duplicate externalPath in user response dto * fixed tests to include externalPath on expected result * fix: unit tests * centralized supported filetypes, perform file type checking of asset and sidecar during file import process * centralized supported filetype check method to keep regex DRY * fixed typo * combined migrations into one * update api * Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not * update mimetype * Fixed detect correct mimetype * revert asset-upload config * reverted domain.constant * refactor * fix mime-type issue * fix format --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
245
web/src/api/open-api/api.ts
generated
245
web/src/api/open-api/api.ts
generated
@@ -979,6 +979,12 @@ export interface CreateUserDto {
|
||||
* @memberof CreateUserDto
|
||||
*/
|
||||
'storageLabel'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CreateUserDto
|
||||
*/
|
||||
'externalPath'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -1294,6 +1300,87 @@ export interface GetAssetCountByTimeBucketDto {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface ImportAssetDto
|
||||
*/
|
||||
export interface ImportAssetDto {
|
||||
/**
|
||||
*
|
||||
* @type {AssetTypeEnum}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'assetType': AssetTypeEnum;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'isReadOnly'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'assetPath': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'sidecarPath'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'deviceAssetId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'deviceId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'fileCreatedAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'fileModifiedAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'isFavorite': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'isArchived'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'isVisible'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportAssetDto
|
||||
*/
|
||||
'duration'?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -2736,6 +2823,12 @@ export interface UpdateUserDto {
|
||||
* @memberof UpdateUserDto
|
||||
*/
|
||||
'storageLabel'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UpdateUserDto
|
||||
*/
|
||||
'externalPath'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -2841,6 +2934,12 @@ export interface UserResponseDto {
|
||||
* @memberof UserResponseDto
|
||||
*/
|
||||
'storageLabel': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserResponseDto
|
||||
*/
|
||||
'externalPath': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -5412,6 +5511,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ImportAssetDto} importAssetDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
importFile: async (importAssetDto: ImportAssetDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'importAssetDto' is not null or undefined
|
||||
assertParamExists('importFile', 'importAssetDto', importAssetDto)
|
||||
const localVarPath = `/asset/import`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(importAssetDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -5565,26 +5708,29 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
* @param {File} assetData
|
||||
* @param {string} fileExtension
|
||||
* @param {string} deviceAssetId
|
||||
* @param {string} deviceId
|
||||
* @param {string} fileCreatedAt
|
||||
* @param {string} fileModifiedAt
|
||||
* @param {boolean} isFavorite
|
||||
* @param {string} fileExtension
|
||||
* @param {string} [key]
|
||||
* @param {File} [livePhotoData]
|
||||
* @param {File} [sidecarData]
|
||||
* @param {boolean} [isReadOnly]
|
||||
* @param {boolean} [isArchived]
|
||||
* @param {boolean} [isVisible]
|
||||
* @param {string} [duration]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
uploadFile: async (assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'assetType' is not null or undefined
|
||||
assertParamExists('uploadFile', 'assetType', assetType)
|
||||
// verify required parameter 'assetData' is not null or undefined
|
||||
assertParamExists('uploadFile', 'assetData', assetData)
|
||||
// verify required parameter 'fileExtension' is not null or undefined
|
||||
assertParamExists('uploadFile', 'fileExtension', fileExtension)
|
||||
// verify required parameter 'deviceAssetId' is not null or undefined
|
||||
assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
|
||||
// verify required parameter 'deviceId' is not null or undefined
|
||||
@@ -5595,8 +5741,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt)
|
||||
// verify required parameter 'isFavorite' is not null or undefined
|
||||
assertParamExists('uploadFile', 'isFavorite', isFavorite)
|
||||
// verify required parameter 'fileExtension' is not null or undefined
|
||||
assertParamExists('uploadFile', 'fileExtension', fileExtension)
|
||||
const localVarPath = `/asset/upload`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@@ -5640,6 +5784,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
localVarFormParams.append('sidecarData', sidecarData as any);
|
||||
}
|
||||
|
||||
if (isReadOnly !== undefined) {
|
||||
localVarFormParams.append('isReadOnly', isReadOnly as any);
|
||||
}
|
||||
|
||||
if (fileExtension !== undefined) {
|
||||
localVarFormParams.append('fileExtension', fileExtension as any);
|
||||
}
|
||||
|
||||
if (deviceAssetId !== undefined) {
|
||||
localVarFormParams.append('deviceAssetId', deviceAssetId as any);
|
||||
}
|
||||
@@ -5668,10 +5820,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
localVarFormParams.append('isVisible', isVisible as any);
|
||||
}
|
||||
|
||||
if (fileExtension !== undefined) {
|
||||
localVarFormParams.append('fileExtension', fileExtension as any);
|
||||
}
|
||||
|
||||
if (duration !== undefined) {
|
||||
localVarFormParams.append('duration', duration as any);
|
||||
}
|
||||
@@ -5909,6 +6057,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ImportAssetDto} importAssetDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async importFile(importAssetDto: ImportAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -5947,23 +6105,24 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
* @param {File} assetData
|
||||
* @param {string} fileExtension
|
||||
* @param {string} deviceAssetId
|
||||
* @param {string} deviceId
|
||||
* @param {string} fileCreatedAt
|
||||
* @param {string} fileModifiedAt
|
||||
* @param {boolean} isFavorite
|
||||
* @param {string} fileExtension
|
||||
* @param {string} [key]
|
||||
* @param {File} [livePhotoData]
|
||||
* @param {File} [sidecarData]
|
||||
* @param {boolean} [isReadOnly]
|
||||
* @param {boolean} [isArchived]
|
||||
* @param {boolean} [isVisible]
|
||||
* @param {string} [duration]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options);
|
||||
async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
@@ -6166,6 +6325,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> {
|
||||
return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {ImportAssetDto} importAssetDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
importFile(importAssetDto: ImportAssetDto, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
||||
return localVarFp.importFile(importAssetDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -6201,23 +6369,24 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
* @param {File} assetData
|
||||
* @param {string} fileExtension
|
||||
* @param {string} deviceAssetId
|
||||
* @param {string} deviceId
|
||||
* @param {string} fileCreatedAt
|
||||
* @param {string} fileModifiedAt
|
||||
* @param {boolean} isFavorite
|
||||
* @param {string} fileExtension
|
||||
* @param {string} [key]
|
||||
* @param {File} [livePhotoData]
|
||||
* @param {File} [sidecarData]
|
||||
* @param {boolean} [isReadOnly]
|
||||
* @param {boolean} [isArchived]
|
||||
* @param {boolean} [isVisible]
|
||||
* @param {string} [duration]
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
uploadFile(assetType: AssetTypeEnum, assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, fileExtension: string, key?: string, livePhotoData?: File, sidecarData?: File, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
||||
return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, fileExtension, key, livePhotoData, sidecarData, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
|
||||
uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
|
||||
return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -6537,6 +6706,20 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest {
|
||||
readonly deviceId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for importFile operation in AssetApi.
|
||||
* @export
|
||||
* @interface AssetApiImportFileRequest
|
||||
*/
|
||||
export interface AssetApiImportFileRequest {
|
||||
/**
|
||||
*
|
||||
* @type {ImportAssetDto}
|
||||
* @memberof AssetApiImportFile
|
||||
*/
|
||||
readonly importAssetDto: ImportAssetDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for searchAsset operation in AssetApi.
|
||||
* @export
|
||||
@@ -6627,6 +6810,13 @@ export interface AssetApiUploadFileRequest {
|
||||
*/
|
||||
readonly assetData: File
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetApiUploadFile
|
||||
*/
|
||||
readonly fileExtension: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -6662,13 +6852,6 @@ export interface AssetApiUploadFileRequest {
|
||||
*/
|
||||
readonly isFavorite: boolean
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetApiUploadFile
|
||||
*/
|
||||
readonly fileExtension: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -6690,6 +6873,13 @@ export interface AssetApiUploadFileRequest {
|
||||
*/
|
||||
readonly sidecarData?: File
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof AssetApiUploadFile
|
||||
*/
|
||||
readonly isReadOnly?: boolean
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
@@ -6934,6 +7124,17 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AssetApiImportFileRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AssetApiSearchAssetRequest} requestParameters Request parameters.
|
||||
@@ -6975,7 +7176,7 @@ export class AssetApi extends BaseAPI {
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.fileExtension, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
|
||||
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,14 +19,15 @@
|
||||
|
||||
const editUser = async () => {
|
||||
try {
|
||||
const { id, email, firstName, lastName, storageLabel } = user;
|
||||
const { id, email, firstName, lastName, storageLabel, externalPath } = user;
|
||||
const { status } = await api.userApi.updateUser({
|
||||
updateUserDto: {
|
||||
id,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
storageLabel: storageLabel || ''
|
||||
storageLabel: storageLabel || '',
|
||||
externalPath: externalPath || ''
|
||||
}
|
||||
});
|
||||
|
||||
@@ -131,6 +132,22 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="external-path">External Path</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="external-path"
|
||||
name="external-path"
|
||||
type="text"
|
||||
bind:value={user.externalPath}
|
||||
/>
|
||||
|
||||
<p>
|
||||
Note: Absolute path of parent import directory. A user can only import files if they exist
|
||||
at or under this path.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-red-400 ml-4 text-sm">{error}</p>
|
||||
{/if}
|
||||
|
||||
@@ -75,6 +75,14 @@
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL PATH"
|
||||
disabled={true}
|
||||
value={user.externalPath || ''}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button type="submit" size="sm" on:click={() => handleSaveProfile()}>Save</Button>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import PencilOutline from 'svelte-material-icons/PencilOutline.svelte';
|
||||
import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte';
|
||||
import Check from 'svelte-material-icons/Check.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
|
||||
@@ -171,6 +173,7 @@
|
||||
<th class="text-center w-1/4 font-medium text-sm">Email</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">First name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Last name</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Can import</th>
|
||||
<th class="text-center w-1/4 font-medium text-sm">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -191,6 +194,15 @@
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.email}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.firstName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
<div class="container flex flex-wrap mx-auto justify-center">
|
||||
{#if user.externalPath}
|
||||
<Check size="16" />
|
||||
{:else}
|
||||
<Close size="16" />
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm px-4 w-1/4 text-ellipsis">
|
||||
{#if !isDeleted(user)}
|
||||
<button
|
||||
|
||||
@@ -8,6 +8,7 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
||||
firstName: Sync.each(() => faker.name.firstName()),
|
||||
lastName: Sync.each(() => faker.name.lastName()),
|
||||
storageLabel: Sync.each(() => faker.random.alphaNumeric()),
|
||||
externalPath: Sync.each(() => faker.random.alphaNumeric()),
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: Sync.each(() => faker.datatype.boolean()),
|
||||
isAdmin: true,
|
||||
|
||||
Reference in New Issue
Block a user